bridgy-fed/tests/test_activitypub.py

2414 wiersze
93 KiB
Python
Czysty Zwykły widok Historia

"""Unit tests for activitypub.py."""
from base64 import b64encode
import copy
from datetime import datetime, timedelta
from hashlib import sha256
import logging
from unittest import skip
noop, lint fixes from flake8 remaining: $ flake8 --extend-ignore=E501 *.py tests/*.py "pyflakes" failed during execution due to "'FlakesChecker' object has no attribute 'NAMEDEXPR'" Run flake8 with greater verbosity to see more details activitypub.py:15:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused activitypub.py:36:1: F401 'web' imported but unused activitypub.py:48:1: E302 expected 2 blank lines, found 1 activitypub.py:51:9: F811 redefinition of unused 'web' from line 36 app.py:6:1: F401 'flask_app.app' imported but unused app.py:9:1: F401 'activitypub' imported but unused app.py:9:1: F401 'convert' imported but unused app.py:9:1: F401 'follow' imported but unused app.py:9:1: F401 'pages' imported but unused app.py:9:1: F401 'redirect' imported but unused app.py:9:1: F401 'superfeedr' imported but unused app.py:9:1: F401 'ui' imported but unused app.py:9:1: F401 'webfinger' imported but unused app.py:9:1: F401 'web' imported but unused app.py:9:1: F401 'xrpc_actor' imported but unused app.py:9:1: F401 'xrpc_feed' imported but unused app.py:9:1: F401 'xrpc_graph' imported but unused app.py:9:19: E401 multiple imports on one line models.py:19:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused models.py:364:31: E114 indentation is not a multiple of four (comment) models.py:364:31: E116 unexpected indentation (comment) protocol.py:17:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused redirect.py:26:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused web.py:18:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused webfinger.py:13:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused webfinger.py:110:13: E122 continuation line missing indentation or outdented webfinger.py:111:13: E122 continuation line missing indentation or outdented webfinger.py:131:13: E122 continuation line missing indentation or outdented webfinger.py:132:13: E122 continuation line missing indentation or outdented webfinger.py:133:13: E122 continuation line missing indentation or outdented webfinger.py:134:13: E122 continuation line missing indentation or outdented tests/__init__.py:2:1: F401 'oauth_dropins.webutil.tests' imported but unused tests/test_follow.py:11:1: F401 'oauth_dropins.webutil.util.json_dumps' imported but unused tests/test_follow.py:14:1: F401 '.testutil.Fake' imported but unused tests/test_models.py:156:15: E122 continuation line missing indentation or outdented tests/test_models.py:157:15: E122 continuation line missing indentation or outdented tests/test_models.py:158:11: E122 continuation line missing indentation or outdented tests/test_web.py:12:1: F401 'oauth_dropins.webutil.util.json_dumps' imported but unused tests/test_web.py:17:1: F401 '.testutil' imported but unused tests/test_web.py:1513:13: E128 continuation line under-indented for visual indent tests/test_web.py:1514:9: E124 closing bracket does not match visual indentation tests/testutil.py:106:1: E402 module level import not at top of file tests/testutil.py:107:1: E402 module level import not at top of file tests/testutil.py:108:1: E402 module level import not at top of file tests/testutil.py:109:1: E402 module level import not at top of file tests/testutil.py:110:1: E402 module level import not at top of file tests/testutil.py:301:24: E203 whitespace before ':' tests/testutil.py:301:25: E701 multiple statements on one line (colon) tests/testutil.py:301:25: E231 missing whitespace after ':'
2023-06-20 18:22:54 +00:00
from unittest.mock import patch
from flask import g
from google.cloud import ndb
from granary import as2, microformats2
from httpsig import HeaderSigner
from oauth_dropins.webutil.testutil import requests_response
from oauth_dropins.webutil.util import domain_from_link, json_dumps, json_loads
from oauth_dropins.webutil import util
import requests
from urllib3.exceptions import ReadTimeoutError
from werkzeug.exceptions import BadGateway, BadRequest
# import first so that Fake is defined before URL routes are registered
from . import testutil
from .testutil import ExplicitEnableFake, Fake, TestCase
import activitypub
from activitypub import (
ActivityPub,
instance_actor,
postprocess_as2,
postprocess_as2_actor,
)
from atproto import ATProto
import common
from models import Follower, Object
import protocol
from web import Web
# have to import module, not attrs, to avoid circular import
from . import test_web
2023-09-23 20:53:17 +00:00
from . import test_webfinger
ACTOR = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'https://mas.to/users/swentel',
'type': 'Person',
'inbox': 'http://mas.to/inbox',
'name': 'Mrs. ☕ Foo',
'icon': {'type': 'Image', 'url': 'https://user.com/me.jpg'},
'image': {'type': 'Image', 'url': 'https://user.com/me.jpg'},
}
ACTOR_AS1 = as2.to_as1(ACTOR)
ACTOR_BASE = {
'@context': [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
],
'type': 'Application',
'id': 'http://localhost/user.com',
'url': 'http://localhost/r/https://user.com/',
'preferredUsername': 'user.com',
'summary': '[<a href="https://fed.brid.gy/web/user.com">bridged</a> from <a href="https://user.com/">user.com</a> by <a href="https://fed.brid.gy/">Bridgy Fed</a>]',
'inbox': 'http://localhost/user.com/inbox',
'outbox': 'http://localhost/user.com/outbox',
'following': 'http://localhost/user.com/following',
'followers': 'http://localhost/user.com/followers',
'endpoints': {
'sharedInbox': 'https://web.brid.gy/ap/sharedInbox',
},
'publicKey': {
'id': 'http://localhost/user.com#key',
'owner': 'http://localhost/user.com',
'publicKeyPem': 'populated in setUp()',
},
}
ACTOR_BASE_FULL = {
**ACTOR_BASE,
'name': 'Ms. ☕ Baz',
'attachment': [{
'name': 'Web site',
'type': 'PropertyValue',
'value': '<a rel="me" href="https://user.com"><span class="invisible">https://</span>user.com</a>',
}],
}
2023-09-23 20:53:17 +00:00
ACTOR_FAKE = {
'@context': [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
],
2023-09-23 20:53:17 +00:00
'type': 'Person',
'id': 'https://fa.brid.gy/ap/fake:user',
'url': 'https://fa.brid.gy/r/fake:user',
'inbox': 'https://fa.brid.gy/ap/fake:user/inbox',
'outbox': 'https://fa.brid.gy/ap/fake:user/outbox',
'following': 'https://fa.brid.gy/ap/fake:user/following',
'followers': 'https://fa.brid.gy/ap/fake:user/followers',
'endpoints': {'sharedInbox': 'https://fa.brid.gy/ap/sharedInbox'},
'preferredUsername': 'fake:handle:user',
2023-09-23 20:53:17 +00:00
'summary': '',
'publicKey': {
'id': 'https://fa.brid.gy/ap/fake:user#key',
'owner': 'https://fa.brid.gy/ap/fake:user',
2023-09-23 20:53:17 +00:00
'publicKeyPem': 'populated in setUp()',
},
}
REPLY_OBJECT = {
'@context': 'https://www.w3.org/ns/activitystreams',
'type': 'Note',
'content': 'A ☕ reply',
'id': 'http://mas.to/reply/id',
'url': 'http://mas.to/reply',
'author': 'https://mas.to/users/swentel',
'inReplyTo': 'https://user.com/post',
'to': [as2.PUBLIC_AUDIENCE],
}
REPLY_OBJECT_WRAPPED = copy.deepcopy(REPLY_OBJECT)
REPLY_OBJECT_WRAPPED['inReplyTo'] = 'http://localhost/r/https://user.com/post'
REPLY = {
'@context': 'https://www.w3.org/ns/activitystreams',
'type': 'Create',
'id': 'http://mas.to/reply/as2',
'object': REPLY_OBJECT,
}
NOTE_OBJECT = {
'@context': 'https://www.w3.org/ns/activitystreams',
'type': 'Note',
'content': '☕ just a normal post',
'id': 'http://mas.to/note/id',
'url': 'http://mas.to/note',
'to': [as2.PUBLIC_AUDIENCE],
'cc': [
'https://mas.to/author/followers',
'https://masto.foo/@other',
'http://localhost/target', # redirect-wrapped
],
}
NOTE = {
'@context': 'https://www.w3.org/ns/activitystreams',
'type': 'Create',
'id': 'http://mas.to/note/as2',
'actor': 'https://masto.foo/@author',
'object': NOTE_OBJECT,
}
MENTION_OBJECT = copy.deepcopy(NOTE_OBJECT)
MENTION_OBJECT.update({
'id': 'http://mas.to/mention/id',
'url': 'http://mas.to/mention',
'tag': [{
'type': 'Mention',
'href': 'https://masto.foo/@other',
'name': '@other@masto.foo',
}, {
'type': 'Mention',
'href': 'http://localhost/tar.get', # redirect-wrapped
'name': '@tar.get@tar.get',
}],
})
MENTION = {
'@context': 'https://www.w3.org/ns/activitystreams',
'type': 'Create',
'id': 'http://mas.to/mention/as2',
'object': MENTION_OBJECT,
}
# based on example Mastodon like:
# https://github.com/snarfed/bridgy-fed/issues/4#issuecomment-334212362
# (reposts are very similar)
LIKE = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'http://mas.to/like#ok',
'type': 'Like',
'object': 'https://user.com/post',
'actor': 'https://mas.to/actor',
}
LIKE_WRAPPED = copy.deepcopy(LIKE)
LIKE_WRAPPED['object'] = 'http://localhost/r/https://user.com/post'
LIKE_ACTOR = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'https://mas.to/actor',
'type': 'Person',
'name': 'Ms. Actor',
'preferredUsername': 'msactor',
'icon': {'type': 'Image', 'url': 'https://user.com/pic.jpg'},
'image': [
{'type': 'Image', 'url': 'https://user.com/thumb.jpg'},
{'type': 'Image', 'url': 'https://user.com/pic.jpg'},
],
}
LIKE_WITH_ACTOR = {
**LIKE,
'actor': LIKE_ACTOR,
}
2023-03-19 23:21:44 +00:00
# repost, should be delivered to followers if object is a fediverse post,
# translated to webmention if object is an indieweb post
REPOST = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'https://mas.to/users/alice/statuses/654/activity',
'type': 'Announce',
'actor': ACTOR['id'],
'object': NOTE_OBJECT['id'],
'published': '2023-02-08T17:44:16Z',
'to': [as2.PUBLIC_AUDIENCE],
}
REPOST_FULL = {
**REPOST,
'actor': ACTOR,
'object': NOTE_OBJECT,
}
FOLLOW = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'https://mas.to/6d1a',
'type': 'Follow',
'actor': ACTOR['id'],
'object': 'https://user.com/',
}
FOLLOW_WRAPPED = copy.deepcopy(FOLLOW)
FOLLOW_WRAPPED['object'] = 'http://localhost/user.com'
FOLLOW_WITH_ACTOR = copy.deepcopy(FOLLOW)
FOLLOW_WITH_ACTOR['actor'] = ACTOR
FOLLOW_WRAPPED_WITH_ACTOR = copy.deepcopy(FOLLOW_WRAPPED)
FOLLOW_WRAPPED_WITH_ACTOR['actor'] = ACTOR
FOLLOW_WITH_OBJECT = copy.deepcopy(FOLLOW)
FOLLOW_WITH_OBJECT['object'] = ACTOR
ACCEPT_FOLLOW = copy.deepcopy(FOLLOW_WITH_ACTOR)
del ACCEPT_FOLLOW['@context']
del ACCEPT_FOLLOW['actor']['@context']
ACCEPT_FOLLOW['actor']['image'] = {'type': 'Image', 'url': 'https://user.com/me.jpg'}
ACCEPT_FOLLOW['object'] = 'http://localhost/user.com'
ACCEPT = {
'@context': 'https://www.w3.org/ns/activitystreams',
'type': 'Accept',
'id': 'http://localhost/r/user.com/followers#accept-https://mas.to/6d1a',
'actor': 'http://localhost/user.com',
'object': {
'type': 'Follow',
'id': 'https://mas.to/6d1a',
'object': 'http://localhost/user.com',
'actor': 'https://mas.to/users/swentel',
'url': 'https://mas.to/users/swentel#followed-user.com',
'to': [as2.PUBLIC_AUDIENCE],
},
'to': [as2.PUBLIC_AUDIENCE],
}
UNDO_FOLLOW_WRAPPED = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'https://mas.to/6d1b',
'type': 'Undo',
'actor': 'https://mas.to/users/swentel',
'object': FOLLOW_WRAPPED,
}
DELETE = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'https://mas.to/users/swentel#delete',
'type': 'Delete',
'actor': 'https://mas.to/users/swentel',
'object': 'https://mas.to/users/swentel',
}
UPDATE_PERSON = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'https://a/person#update',
'type': 'Update',
'actor': 'https://mas.to/users/swentel',
'object': {
'type': 'Person',
'id': 'https://a/person',
},
}
UPDATE_NOTE = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'https://a/note#update',
'type': 'Update',
'actor': 'https://mas.to/users/swentel',
'object': {
'type': 'Note',
'id': 'https://a/note',
},
}
WEBMENTION_DISCOVERY = requests_response(
'<html><head><link rel="webmention" href="/webmention"></html>')
HTML = requests_response('<html></html>', headers={
'Content-Type': common.CONTENT_TYPE_HTML,
})
HTML_WITH_AS2 = requests_response("""\
<html><meta>
<link href='http://as2' rel='alternate' type='application/activity+json'>
</meta></html>
""", headers={
'Content-Type': common.CONTENT_TYPE_HTML,
})
AS2_OBJ = {'foo': ['bar']}
AS2 = requests_response(AS2_OBJ, headers={
'Content-Type': as2.CONTENT_TYPE,
})
NOT_ACCEPTABLE = requests_response(status=406)
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
@patch('requests.post')
@patch('requests.get')
@patch('requests.head')
class ActivityPubTest(TestCase):
def setUp(self):
super().setUp()
self.request_context.push()
self.user = self.make_user('user.com', cls=Web, has_hcard=True,
has_redirects=True,
obj_as1={**ACTOR_AS1, 'id': 'https://user.com/'})
self.swentel_key = ndb.Key(ActivityPub, 'https://mas.to/users/swentel')
self.masto_actor_key = ndb.Key(ActivityPub, 'https://mas.to/actor')
for obj in ACTOR_BASE, ACTOR_FAKE:
obj['publicKey']['publicKeyPem'] = self.user.public_pem().decode()
self.key_id_obj = Object(id='http://my/key/id', as2={
**ACTOR,
'publicKey': {
'id': 'http://my/key/id#unused',
'owner': 'http://own/er',
'publicKeyPem': self.user.public_pem().decode(),
},
})
self.key_id_obj.put()
def assert_object(self, id, **props):
props.setdefault('delivered_protocol', 'web')
return super().assert_object(id, **props)
def sign(self, path, body, host=None):
"""Constructs HTTP Signature, returns headers."""
digest = b64encode(sha256(body.encode()).digest()).decode()
headers = {
'Date': 'Sun, 02 Jan 2022 03:04:05 GMT',
'Host': host or 'localhost',
'Content-Type': as2.CONTENT_TYPE,
'Digest': f'SHA-256={digest}',
}
hs = HeaderSigner('http://my/key/id#unused', self.user.private_pem().decode(),
algorithm='rsa-sha256', sign_header='signature',
headers=('Date', 'Host', 'Digest', '(request-target)'))
return hs.sign(headers, method='POST', path=path)
def post(self, path, json=None, base_url=None, **kwargs):
"""Wrapper around self.client.post that adds signature."""
body = json_dumps(json)
host = domain_from_link(base_url) if base_url else None
headers = self.sign(path, body, host=host)
return self.client.post(path, data=body, headers=headers,
base_url=base_url, **kwargs)
def test_actor_fake(self, *_):
self.make_user('fake:user', cls=Fake)
got = self.client.get('/ap/fake:user', base_url='https://fa.brid.gy/')
self.assertEqual(200, got.status_code, got.get_data(as_text=True))
self.assertEqual(as2.CONTENT_TYPE_LD_PROFILE, got.headers['Content-Type'])
2023-09-23 20:53:17 +00:00
self.assertEqual(ACTOR_FAKE, got.json)
def test_actor_fake_protocol_subdomain(self, *_):
self.make_user('fake:user', cls=Fake)
got = self.client.get('/ap/fake:user', base_url='https://fa.brid.gy/')
self.assertEqual(200, got.status_code)
self.assertEqual(ACTOR_FAKE, got.json)
def test_actor_web(self, *_):
"""Web users are special cased to drop the /web/ prefix."""
got = self.client.get('/user.com')
2021-07-10 15:07:40 +00:00
self.assertEqual(200, got.status_code)
self.assertEqual(as2.CONTENT_TYPE_LD_PROFILE, got.headers['Content-Type'])
self.assertEqual({
**ACTOR_BASE,
'type': 'Person',
'name': 'Mrs. ☕ Foo',
'summary': '',
'icon': {'type': 'Image', 'url': 'https://user.com/me.jpg'},
'image': {'type': 'Image', 'url': 'https://user.com/me.jpg'},
2021-07-10 15:07:40 +00:00
}, got.json)
def test_actor_blocked_tld(self, _, __, ___):
2021-08-18 14:59:52 +00:00
got = self.client.get('/foo.json')
2021-07-10 15:07:40 +00:00
self.assertEqual(404, got.status_code)
def test_actor_new_user_fetch(self, _, mock_get, __):
self.user.obj_key.delete()
self.user.key.delete()
protocol.objects_cache.clear()
mock_get.return_value = requests_response(test_web.ACTOR_HTML)
got = self.client.get('/user.com')
self.assertEqual(200, got.status_code)
self.assert_equals(ACTOR_BASE_FULL, got.json, ignore=['publicKeyPem'])
def test_actor_new_user_fetch_no_mf2(self, _, mock_get, __):
self.user.obj_key.delete()
self.user.key.delete()
protocol.objects_cache.clear()
mock_get.return_value = requests_response('<html></html>')
got = self.client.get('/user.com')
self.assertEqual(200, got.status_code)
self.assert_equals(ACTOR_BASE, got.json, ignore=['publicKeyPem'])
2023-09-23 20:53:17 +00:00
def test_actor_new_user_fetch_fails(self, _, mock_get, ___):
mock_get.side_effect = ReadTimeoutError(None, None, None)
got = self.client.get('/nope.com')
self.assertEqual(504, got.status_code)
2023-09-23 20:53:17 +00:00
def test_actor_handle_existing_user(self, _, __, ___):
self.make_user('fake:user', cls=Fake, obj_as1=as2.to_as1({
**ACTOR,
'id': 'fake:user',
}))
got = self.client.get('/ap/fake:user', base_url='https://fa.brid.gy/')
2023-09-23 20:53:17 +00:00
self.assertEqual(200, got.status_code)
self.assert_equals({
**ACTOR,
**ACTOR_FAKE,
}, got.json, ignore=['publicKeyPem'])
def test_actor_handle_new_user(self, _, __, ___):
2023-11-03 22:11:21 +00:00
Fake.fetchable['fake:user'] = as2.to_as1({
**ACTOR,
'id': 'fake:user',
})
got = self.client.get('/ap/fake:user', base_url='https://fa.brid.gy/')
2023-09-23 20:53:17 +00:00
self.assertEqual(200, got.status_code)
self.assert_equals({
**ACTOR,
**ACTOR_FAKE,
'summary': '[<a href="https://fed.brid.gy/fa/fake:handle:user">bridged</a> from <a href="fake:user">fake:handle:user</a> by <a href="https://fed.brid.gy/">Bridgy Fed</a>]',
'type': 'Application',
2023-09-23 20:53:17 +00:00
}, got.json, ignore=['publicKeyPem'])
def test_actor_atproto_not_enabled(self, *_):
self.store_object(id='did:plc:user', raw={'foo': 'baz'})
self.make_user('did:plc:user', cls=ATProto)
got = self.client.get('/ap/did:plc:user', base_url='https://bsky.brid.gy/')
self.assertEqual(404, got.status_code)
def test_actor_atproto_no_handle(self, *_):
self.store_object(id='did:plc:user', raw={'foo': 'bar'})
self.store_object(id='at://did:plc:user/app.bsky.actor.profile/self', bsky={
'$type': 'app.bsky.actor.profile',
'displayName': 'Alice',
})
self.make_user('did:plc:user', cls=ATProto, enabled_protocols=['activitypub'])
got = self.client.get('/ap/did:plc:user', base_url='https://bsky.brid.gy/')
self.assertEqual(200, got.status_code)
self.assertNotIn('preferredUsername', got.json)
2023-09-23 20:53:17 +00:00
def test_actor_handle_user_fetch_fails(self, _, __, ___):
got = self.client.get('/ap/fake/fake:nope')
2023-09-23 20:53:17 +00:00
self.assertEqual(404, got.status_code)
def test_actor_no_matching_protocol(self, *_):
resp = self.client.get('/foo.json',
base_url='https://bridgy-federated.appspot.com/')
self.assertEqual(404, resp.status_code)
def test_actor_web_redirects(self, *_):
resp = self.client.get('/ap/user.com')
self.assertEqual(301, resp.status_code)
self.assertEqual('https://fed.brid.gy/user.com', resp.headers['Location'])
self.user.ap_subdomain = 'web'
self.user.put()
resp = self.client.get('/user.com', base_url='https://fed.brid.gy/')
self.assertEqual(302, resp.status_code)
self.assertEqual('https://web.brid.gy/user.com', resp.headers['Location'])
self.user.ap_subdomain = 'fed'
self.user.put()
got = self.client.get('/user.com', base_url='https://web.brid.gy/')
self.assertEqual(302, got.status_code)
self.assertEqual('https://fed.brid.gy/user.com', got.headers['Location'])
def test_actor_opted_out(self, *_):
self.user.obj.our_as1['summary'] = '#nobridge'
self.user.obj.put()
self.user.put()
got = self.client.get('/user.com')
self.assertEqual(404, got.status_code)
def test_instance_actor_fetch(self, *_):
def reset_instance_actor():
activitypub._INSTANCE_ACTOR = testutil.global_user
self.addCleanup(reset_instance_actor)
actor_as2 = json_loads(util.read('fed.brid.gy.as2.json'))
self.make_user(common.PRIMARY_DOMAIN, cls=Web, obj_as2=actor_as2)
activitypub._INSTANCE_ACTOR = None
got = self.client.get(f'/{common.PRIMARY_DOMAIN}')
self.assertEqual(200, got.status_code)
self.assert_equals({
**actor_as2,
'id': 'http://localhost/fed.brid.gy',
}, got.json, ignore=['inbox', 'outbox', 'endpoints', 'followers',
'following', 'publicKey', 'publicKeyPem'])
def test_individual_inbox_no_user(self, mock_head, mock_get, mock_post):
self.user.key.delete()
mock_get.side_effect = [self.as2_resp(LIKE_ACTOR)]
reply = {
**REPLY,
'actor': LIKE_ACTOR,
}
self._test_inbox_reply(reply, mock_head, mock_get, mock_post)
self.assert_user(ActivityPub, 'https://mas.to/actor', obj_as2=LIKE_ACTOR)
def test_inbox_activity_without_id(self, *_):
note = copy.deepcopy(NOTE)
del note['id']
resp = self.post('/ap/sharedInbox', json=note)
self.assertEqual(400, resp.status_code)
def test_inbox_reply_object(self, mock_head, mock_get, mock_post):
self._test_inbox_reply(REPLY_OBJECT, mock_head, mock_get, mock_post)
self.assert_object('http://mas.to/reply/id',
source_protocol='activitypub',
our_as1=as2.to_as1(REPLY_OBJECT),
type='comment')
# auto-generated post activity
self.assert_object(
'http://mas.to/reply/id#bridgy-fed-create',
source_protocol='activitypub',
our_as1={
**as2.to_as1({
**REPLY,
'actor': ACTOR,
}),
'id': 'http://mas.to/reply/id#bridgy-fed-create',
'published': '2022-01-02T03:04:05+00:00',
},
status='complete',
delivered=['https://user.com/post'],
type='post',
notify=[self.user.key],
users=[self.swentel_key],
)
def test_inbox_reply_object_wrapped(self, mock_head, mock_get, mock_post):
self._test_inbox_reply(REPLY_OBJECT_WRAPPED, mock_head, mock_get, mock_post)
self.assert_object('http://mas.to/reply/id',
source_protocol='activitypub',
our_as1=as2.to_as1(REPLY_OBJECT),
type='comment')
# auto-generated post activity
self.assert_object(
'http://mas.to/reply/id#bridgy-fed-create',
source_protocol='activitypub',
our_as1={
**as2.to_as1({
**REPLY,
'actor': ACTOR,
}),
'id': 'http://mas.to/reply/id#bridgy-fed-create',
'published': '2022-01-02T03:04:05+00:00',
},
status='complete',
delivered=['https://user.com/post'],
type='post',
notify=[self.user.key],
users=[self.swentel_key],
)
def test_inbox_reply_create_activity(self, mock_head, mock_get, mock_post):
self._test_inbox_reply(REPLY, mock_head, mock_get, mock_post)
self.assert_object('http://mas.to/reply/id',
source_protocol='activitypub',
our_as1=as2.to_as1(REPLY_OBJECT),
type='comment')
# sent activity
self.assert_object(
'http://mas.to/reply/as2',
source_protocol='activitypub',
as2=REPLY,
status='complete',
delivered=['https://user.com/post'],
type='post',
notify=[self.user.key],
users=[self.swentel_key],
)
def _test_inbox_reply(self, reply, mock_head, mock_get, mock_post):
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
mock_head.return_value = requests_response(url='https://user.com/post')
mock_get.side_effect = (
(list(mock_get.side_effect) if mock_get.side_effect
else [self.as2_resp(ACTOR)])
+ [
requests_response(test_web.NOTE_HTML),
requests_response(test_web.NOTE_HTML),
WEBMENTION_DISCOVERY,
])
mock_post.return_value = requests_response()
got = self.post('/ap/web/user.com/inbox', json=reply)
self.assertEqual(202, got.status_code, got.get_data(as_text=True))
self.assert_req(mock_get, 'https://user.com/post')
convert_id = reply['id']
if reply['type'] != 'Create':
convert_id += '%23bridgy-fed-create'
2022-03-17 04:11:09 +00:00
self.assert_req(
mock_post,
'https://user.com/webmention',
2022-03-17 04:11:09 +00:00
headers={'Accept': '*/*'},
allow_redirects=False,
data={
'source': f'https://ap.brid.gy/convert/web/{convert_id}',
'target': 'https://user.com/post',
},
2022-03-17 04:11:09 +00:00
)
def test_inbox_reply_protocol_subdomain(self, mock_head, mock_get, mock_post):
mock_get.return_value = self.as2_resp(ACTOR)
Fake.fetchable['fake:post'] = as2.to_as1({
**NOTE_OBJECT,
'id': 'fake:post',
})
reply = {
**REPLY_OBJECT,
'id': 'http://my/reply',
'inReplyTo': 'fake:post',
}
got = self.post('/ap/fake:user/inbox', json=reply,
base_url='https://fa.brid.gy/')
self.assertEqual(202, got.status_code)
self.assertEqual([('http://my/reply#bridgy-fed-create', 'fake:post:target')],
Fake.sent)
def test_inbox_reply_to_self_domain(self, mock_head, mock_get, mock_post):
mock_get.return_value = test_web.ACTOR_HTML_RESP
self._test_inbox_ignore_reply_to('http://localhost/user.com',
mock_head, mock_get, mock_post)
def test_inbox_reply_to_in_blocklist(self, mock_head, mock_get, mock_post):
mock_get.return_value = HTML
self._test_inbox_ignore_reply_to('https://twitter.com/foo',
mock_head, mock_get, mock_post)
def _test_inbox_ignore_reply_to(self, reply_to, mock_head, mock_get, mock_post):
reply = copy.deepcopy(REPLY_OBJECT)
reply['inReplyTo'] = reply_to
got = self.post('/user.com/inbox', json=reply)
self.assertEqual(204, got.status_code, got.get_data(as_text=True))
mock_post.assert_not_called()
def test_individual_inbox_create_obj(self, *mocks):
self._test_inbox_create_obj('/user.com/inbox', *mocks)
def test_shared_inbox_create_obj(self, *mocks):
self._test_inbox_create_obj('/inbox', *mocks)
def test_ap_sharedInbox_create_obj(self, *mocks):
self._test_inbox_create_obj('/ap/sharedInbox', *mocks)
def _test_inbox_create_obj(self, path, mock_head, mock_get, mock_post):
swentel = self.make_user('https://mas.to/users/swentel', cls=ActivityPub)
Follower.get_or_create(to=swentel, from_=self.user)
bar = self.make_user('fake:bar', cls=Fake, obj_id='fake:bar')
Follower.get_or_create(to=self.make_user('https://other/actor',
cls=ActivityPub),
from_=bar)
baz = self.make_user('fake:baz', cls=Fake, obj_id='fake:baz')
Follower.get_or_create(to=swentel, from_=baz)
baj = self.make_user('fake:baj', cls=Fake, obj_id='fake:baj')
Follower.get_or_create(to=swentel, from_=baj, status='inactive')
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
mock_head.return_value = requests_response(url='http://target')
mock_get.return_value = self.as2_resp(ACTOR) # source actor
mock_post.return_value = requests_response()
got = self.post(path, json=NOTE)
self.assertEqual(202, got.status_code, got.get_data(as_text=True))
expected_obj = {
**as2.to_as1(NOTE_OBJECT),
'author': {'id': 'https://masto.foo/@author'},
'cc': [
{'id': 'https://mas.to/author/followers'},
{'id': 'https://masto.foo/@other'},
{'id': 'target'},
],
}
self.assert_object(NOTE_OBJECT['id'],
source_protocol='activitypub',
our_as1=expected_obj,
type='note',
feed=[self.user.key, baz.key])
expected_create = as2.to_as1(common.unwrap(NOTE))
expected_create.update({
'actor': as2.to_as1(ACTOR),
'object': expected_obj,
})
self.assert_object('http://mas.to/note/as2',
source_protocol='activitypub',
our_as1=expected_create,
users=[ndb.Key(ActivityPub, 'https://masto.foo/@author')],
type='post',
object_ids=[NOTE_OBJECT['id']],
status='complete',
delivered=['shared:target'],
delivered_protocol='fake')
2023-03-19 23:21:44 +00:00
def test_repost_of_indieweb(self, mock_head, mock_get, mock_post):
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
mock_head.return_value = requests_response(url='https://user.com/orig')
mock_get.return_value = WEBMENTION_DISCOVERY
mock_post.return_value = requests_response() # webmention
orig_url = 'https://user.com/orig'
note = {
**NOTE_OBJECT,
2023-03-19 23:21:44 +00:00
'id': 'https://user.com/orig',
}
2023-03-19 23:21:44 +00:00
del note['url']
Object(id=orig_url, mf2=microformats2.object_to_json(as2.to_as1(note)),
source_protocol='web').put()
2023-03-19 23:21:44 +00:00
repost = copy.deepcopy(REPOST_FULL)
repost['object'] = f'http://localhost/r/{orig_url}'
got = self.post('/user.com/inbox', json=repost)
self.assertEqual(202, got.status_code, got.get_data(as_text=True))
self.assert_req(
mock_post,
'https://user.com/webmention',
headers={'Accept': '*/*'},
allow_redirects=False,
data={
'source': f'https://ap.brid.gy/convert/web/{REPOST["id"]}',
'target': orig_url,
},
)
self.assert_object(REPOST_FULL['id'],
source_protocol='activitypub',
status='complete',
as2={
**REPOST,
'actor': ACTOR,
'object': orig_url,
},
users=[self.swentel_key],
delivered=['https://user.com/orig'],
type='share',
2023-03-19 23:21:44 +00:00
object_ids=['https://user.com/orig'])
2023-03-19 23:21:44 +00:00
def test_shared_inbox_repost_of_fediverse(self, mock_head, mock_get, mock_post):
to = self.make_user(ACTOR['id'], cls=ActivityPub)
Follower.get_or_create(to=to, from_=self.user)
baz = self.make_user('fake:baz', cls=Fake, obj_id='fake:baz')
Follower.get_or_create(to=to, from_=baz)
baj = self.make_user('fake:baj', cls=Fake, obj_id='fake:baj')
Follower.get_or_create(to=to, from_=baj, status='inactive')
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
mock_head.return_value = requests_response(url='http://target')
mock_get.return_value = self.as2_resp(NOTE_OBJECT)
got = self.post('/ap/sharedInbox', json=REPOST)
self.assertEqual(202, got.status_code, got.get_data(as_text=True))
2023-03-19 23:21:44 +00:00
mock_post.assert_not_called() # no webmention
self.assert_object(REPOST['id'],
source_protocol='activitypub',
status='complete',
as2=REPOST,
users=[self.swentel_key],
feed=[self.user.key, baz.key],
delivered=['shared:target'],
delivered_protocol='fake',
type='share',
2023-03-19 23:21:44 +00:00
object_ids=[REPOST['object']])
def test_inbox_no_user(self, mock_head, mock_get, mock_post):
mock_get.side_effect = [
# source actor
self.as2_resp(LIKE_WITH_ACTOR['actor']),
# protocol inference
requests_response(test_web.NOTE_HTML),
requests_response(test_web.NOTE_HTML),
2023-03-19 23:21:44 +00:00
# target post webmention discovery
HTML,
]
got = self.post('/ap/sharedInbox', json={
**LIKE,
'object': 'http://nope.com/post',
})
self.assertEqual(202, got.status_code)
2023-03-19 23:21:44 +00:00
self.assert_object('http://mas.to/like#ok',
# no nope.com Web user key since it didn't exist
2023-03-19 23:21:44 +00:00
source_protocol='activitypub',
status='ignored',
our_as1=as2.to_as1({
**LIKE_WITH_ACTOR,
'object': 'http://nope.com/post',
}),
2023-03-19 23:21:44 +00:00
type='like',
notify=[self.user.key],
users=[self.masto_actor_key],
2023-03-19 23:21:44 +00:00
object_ids=['http://nope.com/post'])
2024-02-27 20:05:18 +00:00
def test_inbox_private(self, *mocks):
self._test_inbox_with_to_ignored([], *mocks)
def test_inbox_unlisted(self, *mocks):
self._test_inbox_with_to_ignored(['@unlisted'], *mocks)
def test_inbox_dm(self, *mocks):
self._test_inbox_with_to_ignored(['http://localhost/web/user.com'], *mocks)
2024-02-27 20:05:18 +00:00
def _test_inbox_with_to_ignored(self, to, mock_head, mock_get, mock_post):
Follower.get_or_create(to=self.make_user(ACTOR['id'], cls=ActivityPub),
from_=self.user)
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
mock_head.return_value = requests_response(url='http://target')
mock_get.return_value = self.as2_resp(ACTOR) # source actor
not_public = copy.deepcopy(NOTE)
2024-02-27 20:05:18 +00:00
not_public['object']['to'] = to
got = self.post('/user.com/inbox', json=not_public)
self.assertEqual(200, got.status_code, got.get_data(as_text=True))
self.assertIsNone(Object.get_by_id(not_public['id']))
self.assertIsNone(Object.get_by_id(not_public['object']['id']))
def test_inbox_dm_yes_to_bot_user_enables_protocol(self, *mocks):
user = self.make_user(ACTOR['id'], cls=ActivityPub)
self.assertFalse(ActivityPub.is_enabled_to(ExplicitEnableFake, user))
got = self.post('/ap/sharedInbox', json={
'type': 'Create',
'id': 'https://mas.to/dm#create',
'to': ['https://eefake.brid.gy/eefake.brid.gy'],
'object': {
'type': 'Note',
'id': 'https://mas.to/dm',
'attributedTo': ACTOR['id'],
'to': ['https://eefake.brid.gy/eefake.brid.gy'],
'content': 'yes',
},
})
self.assertEqual(200, got.status_code, got.get_data(as_text=True))
user = user.key.get()
self.assertTrue(ActivityPub.is_enabled_to(ExplicitEnableFake, user))
def test_inbox_actor_blocklisted(self, mock_head, mock_get, mock_post):
got = self.post('/ap/sharedInbox', json={
'type': 'Delete',
'id': 'http://inst/foo#delete',
'actor': 'http://localhost:3000/foo',
'object': 'http://inst/foo',
})
self.assertEqual(400, got.status_code, got.get_data(as_text=True))
self.assertIsNone(Object.get_by_id('http://localhost:3000/foo'))
self.assertIsNone(Object.get_by_id('http://inst/foo#delete'))
self.assertIsNone(Object.get_by_id('http://inst/foo'))
def test_inbox_like(self, mock_head, mock_get, mock_post):
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
mock_head.return_value = requests_response(url='https://user.com/post')
mock_get.side_effect = [
# source actor
self.as2_resp(LIKE_WITH_ACTOR['actor']),
requests_response(test_web.NOTE_HTML),
requests_response(test_web.NOTE_HTML),
WEBMENTION_DISCOVERY,
]
mock_post.return_value = requests_response()
got = self.post('/user.com/inbox', json=LIKE)
self.assertEqual(202, got.status_code)
self.assertIn(self.as2_req('https://mas.to/actor'), mock_get.mock_calls)
self.assertIn(self.req('https://user.com/post'), mock_get.mock_calls)
args, kwargs = mock_post.call_args
self.assertEqual(('https://user.com/webmention',), args)
self.assertEqual({
'source': 'https://ap.brid.gy/convert/web/http://mas.to/like%23ok',
'target': 'https://user.com/post',
}, kwargs['data'])
self.assert_object('http://mas.to/like#ok',
notify=[self.user.key],
users=[self.masto_actor_key],
source_protocol='activitypub',
status='complete',
our_as1=as2.to_as1(LIKE_WITH_ACTOR),
delivered=['https://user.com/post'],
type='like',
object_ids=[LIKE['object']])
def test_inbox_like_indirect_user_creates_User(self, mock_get, *_):
self.user.direct = False
self.user.put()
mock_get.return_value = self.as2_resp(LIKE_ACTOR)
self.test_inbox_like()
self.assert_user(ActivityPub, 'https://mas.to/actor', obj_as2=LIKE_ACTOR)
def test_inbox_like_no_object_error(self, *_):
Fake.fetchable = {'fake:user': {'id': 'fake:user'}}
got = self.post('/inbox', json={
'id': 'fake:like',
'type': 'Like',
'actor': 'fake:user',
'object': None,
})
self.assertEqual(400, got.status_code)
def test_inbox_follow_accept_with_id(self, *mocks):
self._test_inbox_follow_accept(FOLLOW_WRAPPED, ACCEPT, *mocks)
follow = {
**FOLLOW_WITH_ACTOR,
'url': 'https://mas.to/users/swentel#followed-user.com',
}
self.assert_object('https://mas.to/6d1a',
users=[self.swentel_key],
notify=[self.user.key],
source_protocol='activitypub',
status='complete',
our_as1=as2.to_as1(follow),
delivered=['https://user.com/'],
type='follow',
object_ids=[FOLLOW['object']])
def test_inbox_follow_accept_with_object(self, *mocks):
follow = {
**FOLLOW,
'object': {
'id': FOLLOW['object'],
'url': FOLLOW['object'],
},
}
accept = copy.deepcopy(ACCEPT)
accept['object']['url'] = 'https://mas.to/users/swentel#followed-https://user.com/'
self._test_inbox_follow_accept(follow, accept, *mocks)
follow.update({
'actor': ACTOR,
'url': 'https://mas.to/users/swentel#followed-https://user.com/',
})
self.assert_object('https://mas.to/6d1a',
users=[self.swentel_key],
notify=[self.user.key],
source_protocol='activitypub',
status='complete',
our_as1=as2.to_as1(follow),
delivered=['https://user.com/'],
type='follow',
object_ids=[FOLLOW['object']])
def test_inbox_follow_accept_shared_inbox(self, mock_head, mock_get, mock_post):
self._test_inbox_follow_accept(FOLLOW_WRAPPED, ACCEPT,
mock_head, mock_get, mock_post,
inbox_path='/ap/sharedInbox')
url = 'https://mas.to/users/swentel#followed-user.com'
self.assert_object('https://mas.to/6d1a',
users=[self.swentel_key],
notify=[self.user.key],
source_protocol='activitypub',
status='complete',
our_as1=as2.to_as1({**FOLLOW_WITH_ACTOR, 'url': url}),
delivered=['https://user.com/'],
type='follow',
object_ids=[FOLLOW['object']])
def test_inbox_follow_accept_webmention_fails(self, mock_head, mock_get,
mock_post):
mock_post.side_effect = [
requests_response(), # AP Accept
requests.ConnectionError(), # webmention
]
self._test_inbox_follow_accept(FOLLOW_WRAPPED, ACCEPT,
mock_head, mock_get, mock_post)
url = 'https://mas.to/users/swentel#followed-user.com'
self.assert_object('https://mas.to/6d1a',
users=[self.swentel_key],
notify=[self.user.key],
source_protocol='activitypub',
status='failed',
our_as1=as2.to_as1({**FOLLOW_WITH_ACTOR, 'url': url}),
delivered=[],
failed=['https://user.com/'],
type='follow',
object_ids=[FOLLOW['object']])
def _test_inbox_follow_accept(self, follow_as2, accept_as2, mock_head,
mock_get, mock_post, inbox_path='/user.com/inbox'):
# this should makes us make the follower ActivityPub as direct=True
self.user.direct = False
self.user.put()
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
mock_head.return_value = requests_response(url='https://user.com/')
mock_get.side_effect = [
self.as2_resp(ACTOR), # source actor
WEBMENTION_DISCOVERY,
]
if not mock_post.return_value and not mock_post.side_effect:
mock_post.return_value = requests_response()
got = self.post(inbox_path, json=follow_as2)
self.assertEqual(202, got.status_code)
mock_get.assert_has_calls((
self.as2_req(FOLLOW['actor']),
))
# check AP Accept
self.assertEqual(2, len(mock_post.call_args_list))
args, kwargs = mock_post.call_args_list[0]
self.assertEqual(('http://mas.to/inbox',), args)
self.assertEqual(accept_as2, json_loads(kwargs['data']))
# check webmention
args, kwargs = mock_post.call_args_list[1]
self.assertEqual(('https://user.com/webmention',), args)
self.assertEqual({
'source': 'https://ap.brid.gy/convert/web/https://mas.to/6d1a',
'target': 'https://user.com/',
}, kwargs['data'])
# check that we stored Follower and ActivityPub user for the follower
self.assert_entities_equal(
Follower(to=self.user.key,
from_=ActivityPub(id=ACTOR['id']).key,
status='active',
follow=Object(id=FOLLOW['id']).key),
Follower.query().fetch(),
ignore=['created', 'updated'])
self.assert_user(ActivityPub, 'https://mas.to/users/swentel',
obj_as2=ACTOR, direct=False)
self.assert_user(Web, 'user.com', direct=False,
has_hcard=True, has_redirects=True)
def test_inbox_follow_use_instead_strip_www(self, mock_head, mock_get, mock_post):
self.make_user('www.user.com', cls=Web, use_instead=self.user.key)
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
mock_head.return_value = requests_response(url='https://www.user.com/')
mock_get.side_effect = [
# source actor
self.as2_resp(ACTOR),
# target user
test_web.ACTOR_HTML_RESP,
# target post webmention discovery
requests_response('<html></html>'),
]
mock_post.return_value = requests_response()
got = self.post('/user.com/inbox', json=FOLLOW_WRAPPED)
self.assertEqual(202, got.status_code)
follower = Follower.query().get()
self.assert_entities_equal(
Follower(to=self.user.key,
from_=ActivityPub(id=ACTOR['id']).key,
status='active',
follow=Object(id=FOLLOW['id']).key),
follower,
ignore=['created', 'updated'])
# double check that Follower doesn't have www
self.assertEqual('user.com', follower.to.id())
# double check that follow Object doesn't have www
self.assertEqual('active', follower.status)
self.assertEqual('https://mas.to/users/swentel#followed-user.com',
follower.follow.get().as2['url'])
def test_inbox_follow_web_brid_gy_subdomain(self, mock_head, mock_get, mock_post):
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
mock_head.return_value = requests_response(url='https://user.com/')
mock_get.side_effect = [
# source actor
self.as2_resp(ACTOR),
# target user
test_web.ACTOR_HTML_RESP,
# target post webmention discovery
requests_response('<html></html>'),
]
mock_post.return_value = requests_response()
got = self.post('/user.com/inbox', base_url='https://web.brid.gy/', json={
**FOLLOW_WRAPPED,
'object': 'https://web.brid.gy/user.com',
})
self.assertEqual(202, got.status_code)
# check that AP Accept uses web.brid.gy, not fed.brid.gy
args, kwargs = mock_post.call_args_list[0]
self.assert_equals(('http://mas.to/inbox',), args)
self.assert_equals({
'type': 'Accept',
'id': 'https://web.brid.gy/r/user.com/followers#accept-https://mas.to/6d1a',
'actor': 'https://web.brid.gy/user.com',
'object': {
'type': 'Follow',
'id': 'https://mas.to/6d1a',
'object': 'https://web.brid.gy/user.com',
'actor': 'https://mas.to/users/swentel',
'url': 'https://mas.to/users/swentel#followed-user.com',
},
}, json_loads(kwargs['data']), ignore=['to', '@context'])
def test_inbox_undo_follow(self, mock_head, mock_get, mock_post):
follower = Follower(to=self.user.key,
from_=ActivityPub(id=ACTOR['id']).key,
status='active')
follower.put()
mock_get.side_effect = [
self.as2_resp(ACTOR),
test_web.ACTOR_HTML_RESP,
WEBMENTION_DISCOVERY,
]
mock_post.return_value = requests_response()
got = self.post('/user.com/inbox', json=UNDO_FOLLOW_WRAPPED)
self.assertEqual(202, got.status_code)
# check that the Follower is now inactive
self.assertEqual('inactive', follower.key.get().status)
def test_inbox_follow_inactive(self, mock_head, mock_get, mock_post):
follower = Follower.get_or_create(
to=self.user,
from_=self.make_user(ACTOR['id'], cls=ActivityPub, obj_as2=ACTOR),
status='inactive')
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
mock_head.return_value = requests_response(url='https://user.com/')
mock_get.side_effect = [
test_web.ACTOR_HTML_RESP,
WEBMENTION_DISCOVERY,
]
mock_post.return_value = requests_response()
got = self.post('/user.com/inbox', json=FOLLOW_WRAPPED)
self.assertEqual(202, got.status_code)
# check that the Follower is now active
self.assertEqual('active', follower.key.get().status)
def test_inbox_undo_follow_doesnt_exist(self, mock_head, mock_get, mock_post):
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
mock_head.return_value = requests_response(url='https://user.com/')
mock_get.side_effect = [
self.as2_resp(ACTOR),
test_web.ACTOR_HTML_RESP,
WEBMENTION_DISCOVERY,
]
mock_post.return_value = requests_response()
got = self.post('/user.com/inbox', json=UNDO_FOLLOW_WRAPPED)
self.assertEqual(202, got.status_code)
def test_inbox_undo_follow_inactive(self, mock_head, mock_get, mock_post):
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
mock_head.return_value = requests_response(url='https://user.com/')
mock_get.side_effect = [
self.as2_resp(ACTOR),
test_web.ACTOR_HTML_RESP,
WEBMENTION_DISCOVERY,
]
mock_post.return_value = requests_response()
follower = Follower.get_or_create(to=self.user,
from_=ActivityPub.get_or_create(ACTOR['id']),
status='inactive')
got = self.post('/user.com/inbox', json=UNDO_FOLLOW_WRAPPED)
self.assertEqual(202, got.status_code)
self.assertEqual('inactive', follower.key.get().status)
def test_inbox_undo_follow_composite_object(self, mock_head, mock_get, mock_post):
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
mock_head.return_value = requests_response(url='https://user.com/')
mock_get.side_effect = [
self.as2_resp(ACTOR),
test_web.ACTOR_HTML_RESP,
WEBMENTION_DISCOVERY,
]
mock_post.return_value = requests_response()
follower_key = ActivityPub.get_or_create(ACTOR['id'])
follower = Follower.get_or_create(to=self.user, from_=follower_key,
status='inactive')
undo_follow = copy.deepcopy(UNDO_FOLLOW_WRAPPED)
undo_follow['object']['object'] = {'id': undo_follow['object']['object']}
got = self.post('/user.com/inbox', json=undo_follow)
self.assertEqual(202, got.status_code)
self.assertEqual('inactive', follower.key.get().status)
def test_inbox_unsupported_type(self, mock_head, mock_get, mock_post):
mock_get.return_value = self.as2_resp(ACTOR)
got = self.post('/user.com/inbox', json={
'@context': ['https://www.w3.org/ns/activitystreams'],
'id': 'https://xoxo.zone/users/aaronpk#follows/40',
'type': 'Arrive',
'actor': 'https://xoxo.zone/users/aaronpk',
'object': 'http://a/place',
2021-07-10 15:07:40 +00:00
})
self.assertEqual(501, got.status_code)
def test_inbox_bad_object_url(self, mock_head, mock_get, mock_post):
# https://console.cloud.google.com/errors/detail/CMKn7tqbq-GIRA;time=P30D?project=bridgy-federated
mock_get.return_value = self.as2_resp(ACTOR) # source actor
id = 'https://mas.to/users/tmichellemoore#likes/56486252'
bad_url = 'http://localhost/r/Testing \u2013 Brid.gy \u2013 Post to Mastodon 3'
bad = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': id,
'type': 'Like',
'actor': ACTOR['id'],
'object': bad_url,
}
got = self.post('/user.com/inbox', json=bad)
# bad object, should ignore activity
self.assertEqual(204, got.status_code)
mock_post.assert_not_called()
expected = {
**as2.to_as1(bad),
'actor': as2.to_as1(ACTOR),
'object': 'https://Testing – Brid.gy – Post to Mastodon 3/',
}
self.assert_object(id,
our_as1=expected,
users=[self.swentel_key],
source_protocol='activitypub',
status='ignored',
)
self.assertIsNone(Object.get_by_id(bad_url))
@patch('activitypub.logger.info', side_effect=logging.info)
@patch('common.logger.info', side_effect=logging.info)
@patch('oauth_dropins.webutil.appengine_info.DEBUG', False)
def test_inbox_verify_http_signature(self, mock_common_log, mock_activitypub_log,
_, mock_get, ___):
# actor with a public key
self.key_id_obj.key.delete()
protocol.objects_cache.clear()
actor_as2 = {
**ACTOR,
'publicKey': {
'id': 'http://my/key/id#unused',
'owner': 'http://own/er',
'publicKeyPem': self.user.public_pem().decode(),
},
}
mock_get.return_value = self.as2_resp(actor_as2)
# valid signature
body = json_dumps(NOTE)
headers = self.sign('/ap/sharedInbox', json_dumps(NOTE))
resp = self.client.post('/ap/sharedInbox', data=body, headers=headers)
self.assertEqual(204, resp.status_code, resp.get_data(as_text=True))
mock_get.assert_has_calls((
self.as2_req('http://my/key/id'),
))
mock_activitypub_log.assert_any_call('HTTP Signature verified!')
# valid signature, Object has no key
self.key_id_obj.as2 = ACTOR
self.key_id_obj.put()
resp = self.client.post('/ap/sharedInbox', data=body, headers=headers)
self.assertEqual(401, resp.status_code, resp.get_data(as_text=True))
# valid signature, Object has our_as1 instead of as2
self.key_id_obj.clear()
self.key_id_obj.our_as1 = as2.to_as1(actor_as2)
self.key_id_obj.put()
resp = self.client.post('/ap/sharedInbox', data=body, headers=headers)
self.assertEqual(204, resp.status_code, resp.get_data(as_text=True))
mock_activitypub_log.assert_any_call('HTTP Signature verified!')
# invalid signature, missing keyId
protocol.seen_ids.clear()
obj_key = ndb.Key(Object, NOTE['id'])
obj_key.delete()
resp = self.client.post('/ap/sharedInbox', data=body, headers={
**headers,
'signature': headers['signature'].replace(
'keyId="http://my/key/id#unused",', ''),
})
self.assertEqual(401, resp.status_code)
self.assertEqual({'error': 'HTTP Signature missing keyId'}, resp.json)
mock_common_log.assert_any_call('Returning 401: HTTP Signature missing keyId', exc_info=None)
# invalid signature, content changed
protocol.seen_ids.clear()
obj_key = ndb.Key(Object, NOTE['id'])
obj_key.delete()
resp = self.client.post('/ap/sharedInbox', json={**NOTE, 'content': 'z'}, headers=headers)
self.assertEqual(401, resp.status_code)
self.assertEqual({'error': 'Invalid Digest header, required for HTTP Signature'},
resp.json)
mock_common_log.assert_any_call('Returning 401: Invalid Digest header, required for HTTP Signature', exc_info=None)
# invalid signature, header changed
protocol.seen_ids.clear()
obj_key.delete()
resp = self.client.post('/ap/sharedInbox', data=body, headers={**headers, 'Date': 'X'})
self.assertEqual(401, resp.status_code)
self.assertEqual({'error': 'HTTP Signature verification failed'}, resp.json)
mock_common_log.assert_any_call('Returning 401: HTTP Signature verification failed', exc_info=None)
# no signature
protocol.seen_ids.clear()
obj_key.delete()
resp = self.client.post('/ap/sharedInbox', json=NOTE)
self.assertEqual(401, resp.status_code, resp.get_data(as_text=True))
self.assertEqual({'error': 'No HTTP Signature'}, resp.json)
mock_common_log.assert_any_call('Returning 401: No HTTP Signature', exc_info=None)
def test_delete_actor(self, *mocks):
deleted = self.make_user(DELETE['actor'], cls=ActivityPub)
follower = Follower.get_or_create(to=self.user, from_=deleted)
followee = Follower.get_or_create(to=deleted, from_=Fake(id='fake:user'))
# other unrelated follower
other = self.make_user('https://mas.to/users/other', cls=ActivityPub)
other = Follower.get_or_create(to=self.user, from_=other)
self.assertEqual(3, Follower.query().count())
got = self.post('/ap/sharedInbox', json=DELETE)
self.assertEqual(204, got.status_code)
self.assertEqual('inactive', follower.key.get().status)
self.assertEqual('inactive', followee.key.get().status)
self.assertEqual('active', other.key.get().status)
def test_delete_actor_not_fetchable(self, _, mock_get, ___):
self.key_id_obj.key.delete()
protocol.objects_cache.clear()
mock_get.return_value = requests_response(status=410)
got = self.post('/ap/sharedInbox', json={**DELETE, 'object': 'http://my/key/id'})
self.assertEqual(202, got.status_code)
def test_delete_actor_empty_deleted_object(self, _, mock_get, ___):
self.key_id_obj.as2 = None
self.key_id_obj.deleted = True
self.key_id_obj.put()
protocol.objects_cache.clear()
got = self.post('/ap/sharedInbox', json={**DELETE, 'object': 'http://my/key/id'})
self.assertEqual(202, got.status_code)
mock_get.assert_not_called()
def test_delete_note(self, _, mock_get, ___):
obj = Object(id='http://an/obj')
2023-02-14 22:30:00 +00:00
obj.put()
mock_get.side_effect = [
self.as2_resp(ACTOR),
]
delete = {
**DELETE,
'object': 'http://an/obj',
}
resp = self.post('/ap/sharedInbox', json=delete)
self.assertEqual(204, resp.status_code)
2023-02-14 22:30:00 +00:00
self.assertTrue(obj.key.get().deleted)
self.assert_object(delete['id'],
our_as1={
**as2.to_as1(delete),
'actor': as2.to_as1(ACTOR),
},
type='delete',
source_protocol='activitypub',
status='ignored',
users=[ActivityPub(id='https://mas.to/users/swentel').key])
obj.populate(deleted=True, as2=None)
self.assert_entities_equal(obj,
protocol.objects_cache['http://an/obj'],
ignore=['expire', 'created', 'updated'])
2023-02-14 22:30:00 +00:00
def test_update_note(self, *mocks):
Object(id='https://a/note', as2={}).put()
self._test_update(*mocks)
def test_update_unknown(self, *mocks):
self._test_update(*mocks)
def _test_update(self, _, mock_get, ___):
mock_get.side_effect = [
self.as2_resp(ACTOR),
]
resp = self.post('/ap/sharedInbox', json=UPDATE_NOTE)
self.assertEqual(204, resp.status_code)
note_as1 = as2.to_as1({
**UPDATE_NOTE['object'],
'author': {'id': 'https://mas.to/users/swentel'},
})
self.assert_object('https://a/note',
type='note',
our_as1=note_as1,
source_protocol='activitypub')
update_as1 = {
**as2.to_as1(UPDATE_NOTE),
'object': note_as1,
'actor': as2.to_as1(ACTOR),
}
self.assert_object(UPDATE_NOTE['id'],
source_protocol='activitypub',
type='update',
status='ignored',
our_as1=update_as1,
users=[self.swentel_key])
2023-02-14 22:30:00 +00:00
self.assert_entities_equal(Object.get_by_id('https://a/note'),
protocol.objects_cache['https://a/note'])
2023-02-14 22:30:00 +00:00
def test_inbox_webmention_discovery_connection_fails(self, mock_head,
mock_get, mock_post):
mock_get.side_effect = [
# source actor
self.as2_resp(LIKE_WITH_ACTOR['actor']),
# protocol inference
requests_response(test_web.NOTE_HTML),
requests_response(test_web.NOTE_HTML),
# target post webmention discovery
ReadTimeoutError(None, None, None),
]
got = self.post('/user.com/inbox', json=LIKE)
self.assertEqual(202, got.status_code)
def test_inbox_no_webmention_endpoint(self, mock_head, mock_get, mock_post):
mock_get.side_effect = [
# source actor
self.as2_resp(LIKE_WITH_ACTOR['actor']),
# protocol inference
requests_response(test_web.NOTE_HTML),
requests_response(test_web.NOTE_HTML),
# target post webmention discovery
HTML,
]
got = self.post('/user.com/inbox', json=LIKE)
self.assertEqual(202, got.status_code)
self.assert_object('http://mas.to/like#ok',
notify=[self.user.key],
users=[self.masto_actor_key],
source_protocol='activitypub',
status='ignored',
our_as1=as2.to_as1(LIKE_WITH_ACTOR),
type='like',
object_ids=[LIKE['object']])
def test_inbox_id_already_seen(self, *mocks):
obj_key = Object(id=FOLLOW_WRAPPED['id'], as2={}).put()
got = self.post('/user.com/inbox', json=FOLLOW_WRAPPED)
self.assertEqual(204, got.status_code)
self.assertEqual(0, Follower.query().count())
# second time should use in memory cache
obj_key.delete()
got = self.post('/user.com/inbox', json=FOLLOW_WRAPPED)
self.assertEqual(204, got.status_code)
self.assertEqual(0, Follower.query().count())
def test_inbox_http_sig_is_not_actor_author(self, mock_head, mock_get, mock_post):
mock_get.side_effect = [
self.as2_resp(ACTOR), # author
]
with self.assertLogs() as logs:
got = self.post('/user.com/inbox', json={
**NOTE_OBJECT,
'author': 'https://al/ice',
})
self.assertEqual(204, got.status_code, got.get_data(as_text=True))
self.assertIn(
"WARNING:protocol:actor https://al/ice isn't authed user http://my/key/id",
logs.output)
def test_followers_collection_unknown_user(self, *_):
resp = self.client.get('/nope.com/followers')
self.assertEqual(404, resp.status_code)
def test_followers_collection_empty(self, *_):
resp = self.client.get('/user.com/followers')
self.assertEqual(200, resp.status_code)
self.assertEqual({
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'http://localhost/user.com/followers',
'type': 'Collection',
'summary': "user.com's followers",
'totalItems': 0,
'first': {
'type': 'CollectionPage',
'partOf': 'http://localhost/user.com/followers',
'items': [],
},
}, resp.json)
def store_followers(self):
follow = Object(id=FOLLOW_WITH_ACTOR['id'], as2=FOLLOW_WITH_ACTOR).put()
Follower.get_or_create(
to=self.user,
from_=self.make_user('http://bar', cls=ActivityPub, obj_as2=ACTOR),
follow=follow)
Follower.get_or_create(
to=self.make_user('https://other/actor', cls=ActivityPub),
from_=self.user)
Follower.get_or_create(
to=self.user,
from_=self.make_user('http://baz', cls=ActivityPub, obj_as2=ACTOR),
follow=follow)
Follower.get_or_create(
to=self.user,
from_=self.make_user('fake:baj', cls=Fake),
status='inactive')
def test_followers_collection_fake(self, *_):
self.make_user('fake:foo', cls=Fake)
resp = self.client.get('/ap/fake:foo/followers',
base_url='https://fa.brid.gy')
self.assertEqual(200, resp.status_code)
self.assertEqual({
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'https://fa.brid.gy/ap/fake:foo/followers',
'type': 'Collection',
'summary': "fake:foo's followers",
'totalItems': 0,
'first': {
'type': 'CollectionPage',
'partOf': 'https://fa.brid.gy/ap/fake:foo/followers',
'items': [],
},
}, resp.json)
def test_followers_collection(self, *_):
self.store_followers()
resp = self.client.get('/user.com/followers')
self.assertEqual(200, resp.status_code)
self.assert_equals({
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'http://localhost/user.com/followers',
'type': 'Collection',
'summary': "user.com's followers",
'totalItems': 2,
'first': {
'type': 'CollectionPage',
'partOf': 'http://localhost/user.com/followers',
'items': [ACTOR, ACTOR],
},
}, resp.json)
@patch('models.PAGE_SIZE', 1)
def test_followers_collection_page(self, *_):
self.store_followers()
before = (datetime.utcnow() + timedelta(seconds=1)).isoformat()
next = Follower.query(Follower.from_ == ActivityPub(id='http://baz').key,
Follower.to == self.user.key,
).get().updated.isoformat()
resp = self.client.get(f'/user.com/followers?before={before}')
self.assertEqual(200, resp.status_code)
self.assert_equals({
'@context': 'https://www.w3.org/ns/activitystreams',
'id': f'http://localhost/user.com/followers?before={before}',
'type': 'CollectionPage',
'partOf': 'http://localhost/user.com/followers',
'next': f'http://localhost/user.com/followers?before={next}',
'prev': f'http://localhost/user.com/followers?after={before}',
'items': [ACTOR],
}, resp.json)
def test_following_collection_unknown_user(self, *_):
resp = self.client.get('/nope.com/following')
self.assertEqual(404, resp.status_code)
def test_following_collection_empty(self, *_):
resp = self.client.get('/user.com/following')
self.assertEqual(200, resp.status_code)
self.assertEqual({
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'http://localhost/user.com/following',
'summary': "user.com's following",
'type': 'Collection',
'totalItems': 0,
'first': {
'type': 'CollectionPage',
'partOf': 'http://localhost/user.com/following',
'items': [],
},
}, resp.json)
def store_following(self):
follow = Object(id=FOLLOW_WITH_ACTOR['id'], as2=FOLLOW_WITH_ACTOR).put()
Follower.get_or_create(
to=self.make_user('http://bar', cls=ActivityPub, obj_as2=ACTOR),
from_=self.user,
follow=follow)
Follower.get_or_create(
to=self.user,
from_=self.make_user('https://other/actor', cls=ActivityPub))
Follower.get_or_create(
to=self.make_user('http://baz', cls=ActivityPub, obj_as2=ACTOR),
from_=self.user, follow=follow)
Follower.get_or_create(
to=self.make_user('http://ba/j', cls=ActivityPub),
from_=self.user,
status='inactive')
def test_following_collection(self, *_):
self.store_following()
resp = self.client.get('/user.com/following')
self.assertEqual(200, resp.status_code)
self.assert_equals({
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'http://localhost/user.com/following',
'summary': "user.com's following",
'type': 'Collection',
'totalItems': 2,
'first': {
'type': 'CollectionPage',
'partOf': 'http://localhost/user.com/following',
'items': [ACTOR, ACTOR],
},
}, resp.json)
@patch('models.PAGE_SIZE', 1)
def test_following_collection_page(self, *_):
self.store_following()
after = datetime(1900, 1, 1).isoformat()
prev = Follower.query(Follower.to == ActivityPub(id='http://bar').key,
Follower.from_ == self.user.key,
).get().updated.isoformat()
resp = self.client.get(f'/user.com/following?after={after}')
self.assertEqual(200, resp.status_code)
self.assert_equals({
'@context': 'https://www.w3.org/ns/activitystreams',
'id': f'http://localhost/user.com/following?after={after}',
'type': 'CollectionPage',
'partOf': 'http://localhost/user.com/following',
'prev': f'http://localhost/user.com/following?after={prev}',
'next': f'http://localhost/user.com/following?before={after}',
'items': [ACTOR],
}, resp.json)
2023-01-25 21:12:24 +00:00
def test_following_collection_head(self, *_):
resp = self.client.head(f'/user.com/following')
self.assertEqual(200, resp.status_code)
self.assertEqual('', resp.get_data(as_text=True))
def test_following_collection_opted_out(self, *_):
self.user.obj.our_as1['summary'] = '#nobridge'
self.user.obj.put()
self.user.put()
resp = self.client.get(f'/user.com/following', base_url='https://web.brid.gy')
self.assertEqual(404, resp.status_code)
def test_outbox_fake_empty(self, *_):
self.make_user('fake:foo', cls=Fake)
resp = self.client.get(f'/ap/fake:foo/outbox',
base_url='https://fa.brid.gy')
self.assertEqual(200, resp.status_code)
self.assertEqual({
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'https://fa.brid.gy/ap/fake:foo/outbox',
'summary': "fake:foo's outbox",
'type': 'OrderedCollection',
'totalItems': 0,
'first': {
'type': 'CollectionPage',
'partOf': 'https://fa.brid.gy/ap/fake:foo/outbox',
'items': [],
},
}, resp.json)
def store_outbox_objects(self, user):
for i, obj in enumerate([REPLY, MENTION, LIKE, DELETE]):
self.store_object(id=obj['id'], users=[user.key], as2=obj)
@patch('models.PAGE_SIZE', 2)
def test_outbox_fake_objects(self, *_):
user = self.make_user('fake:foo', cls=Fake)
self.store_outbox_objects(user)
resp = self.client.get(f'/ap/fake:foo/outbox',
base_url='https://fa.brid.gy')
self.assertEqual(200, resp.status_code)
after = Object.get_by_id(LIKE['id']).updated.isoformat()
self.assert_equals({
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'https://fa.brid.gy/ap/fake:foo/outbox',
'summary': "fake:foo's outbox",
'type': 'OrderedCollection',
'totalItems': 4,
'first': {
'type': 'CollectionPage',
'partOf': 'https://fa.brid.gy/ap/fake:foo/outbox',
'items': [DELETE, LIKE],
'next': f'https://fa.brid.gy/ap/fake:foo/outbox?before={after}',
},
}, resp.json)
@patch('models.PAGE_SIZE', 2)
def test_outbox_fake_objects_page(self, *_):
user = self.make_user('fake:foo', cls=Fake)
self.store_outbox_objects(user)
after = datetime(1900, 1, 1).isoformat()
resp = self.client.get(f'/ap/fake:foo/outbox?after={after}',
base_url='https://fa.brid.gy')
self.assertEqual(200, resp.status_code)
prev = Object.get_by_id(MENTION['id']).updated.isoformat()
self.assert_equals({
'@context': 'https://www.w3.org/ns/activitystreams',
'id': f'https://fa.brid.gy/ap/fake:foo/outbox?after={after}',
'type': 'CollectionPage',
'partOf': 'https://fa.brid.gy/ap/fake:foo/outbox',
'prev': f'https://fa.brid.gy/ap/fake:foo/outbox?after={prev}',
'next': f'https://fa.brid.gy/ap/fake:foo/outbox?before={after}',
'items': [MENTION, REPLY],
}, resp.json)
def test_outbox_web_empty(self, *_):
resp = self.client.get(f'/user.com/outbox')
2023-01-25 21:12:24 +00:00
self.assertEqual(200, resp.status_code)
self.assertEqual({
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'http://localhost/user.com/outbox',
'summary': "user.com's outbox",
2023-01-25 21:12:24 +00:00
'type': 'OrderedCollection',
'totalItems': 0,
2023-01-25 21:12:24 +00:00
'first': {
'type': 'CollectionPage',
'partOf': 'http://localhost/user.com/outbox',
2023-01-25 21:12:24 +00:00
'items': [],
},
}, resp.json)
def test_outbox_web_head(self, *_):
resp = self.client.head(f'/user.com/outbox')
self.assertEqual(200, resp.status_code)
self.assertEqual('', resp.get_data(as_text=True))
def test_outbox_opted_out(self, *_):
self.user.obj.our_as1['summary'] = '#nobridge'
self.user.obj.put()
self.user.put()
resp = self.client.get(f'/ap/user.com/outbox',
base_url='https://web.brid.gy')
self.assertEqual(404, resp.status_code)
class ActivityPubUtilsTest(TestCase):
def setUp(self):
super().setUp()
self.user = self.make_user('user.com', cls=Web, has_hcard=True, obj_as2=ACTOR)
for obj in ACTOR_BASE, ACTOR_FAKE:
obj['publicKey']['publicKeyPem'] = self.user.public_pem().decode()
def test_put_validates_id(self, *_):
for bad in (
'',
'not a url',
'ftp://not.web/url',
'https:///no/domain',
'https://fed.brid.gy/foo',
'https://ap.brid.gy/foo',
'http://localhost/foo',
):
with self.assertRaises(AssertionError):
ActivityPub(id=bad).put()
def test_owns_id(self):
self.assertIsNone(ActivityPub.owns_id('http://foo'))
self.assertIsNone(ActivityPub.owns_id('https://bar/baz'))
self.assertFalse(ActivityPub.owns_id('at://did:plc:foo/bar/123'))
self.assertFalse(ActivityPub.owns_id('e45fab982'))
self.assertFalse(ActivityPub.owns_id('https://twitter.com/foo'))
self.assertFalse(ActivityPub.owns_id('https://fed.brid.gy/foo'))
def test_owns_handle(self):
for handle in ('@user@instance', 'user@instance.com', 'user.com@instance.com',
'user@instance'):
with self.subTest(handle=handle):
assert ActivityPub.owns_handle(handle)
for handle in ('instance', 'instance.com', '@user', '@user.com',
'http://user.com', '@user@web.brid.gy', '@user@localhost'):
with self.subTest(handle=handle):
self.assertEqual(False, ActivityPub.owns_handle(handle))
def test_handle_to_id_stored(self):
self.make_user(id='http://inst.com/@user', cls=ActivityPub)
self.assertEqual('http://inst.com/@user',
ActivityPub.handle_to_id('@user@inst.com'))
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
@patch('requests.get')
def test_handle_to_id_fetch(self, mock_get):
2023-09-23 20:53:17 +00:00
mock_get.return_value = requests_response(test_webfinger.WEBFINGER)
self.assertEqual('http://localhost/user.com',
ActivityPub.handle_to_id('@user@inst.com'))
self.assert_req(
mock_get,
'https://inst.com/.well-known/webfinger?resource=acct:user@inst.com')
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
@patch('requests.get', return_value=requests_response({}))
def test_handle_to_id_not_found(self, mock_get):
self.assertIsNone(ActivityPub.handle_to_id('@user@inst.com'))
self.assert_req(
mock_get,
'https://inst.com/.well-known/webfinger?resource=acct:user@inst.com')
def test_postprocess_as2_multiple_in_reply_tos(self):
self.assert_equals({
'id': 'http://localhost/r/xyz',
'inReplyTo': 'foo',
'to': [as2.PUBLIC_AUDIENCE],
2023-06-16 20:16:17 +00:00
}, postprocess_as2({
'id': 'xyz',
'inReplyTo': ['foo', 'bar'],
}))
def test_postprocess_as2_multiple_url(self):
self.assert_equals({
'id': 'http://localhost/r/xyz',
'url': ['http://localhost/r/foo', 'http://localhost/r/bar'],
'to': [as2.PUBLIC_AUDIENCE],
2023-06-16 20:16:17 +00:00
}, postprocess_as2({
'id': 'xyz',
'url': ['foo', 'bar'],
}))
def test_postprocess_as2_multiple_image(self):
self.assert_equals({
'id': 'http://localhost/r/xyz',
'attachment': [{'url': 'http://r/foo'}, {'url': 'http://r/bar'}],
'image': [{'url': 'http://r/foo'}, {'url': 'http://r/bar'}],
'to': [as2.PUBLIC_AUDIENCE],
2023-06-16 20:16:17 +00:00
}, postprocess_as2({
'id': 'xyz',
'image': [{'url': 'http://r/foo'}, {'url': 'http://r/bar'}],
}))
def test_postprocess_as2_note(self):
self.assert_equals({
'id': 'http://localhost/r/xyz',
'type': 'Note',
'content': 'foo',
'contentMap': {'en': 'foo'},
'to': [as2.PUBLIC_AUDIENCE],
2023-06-16 20:16:17 +00:00
}, postprocess_as2({
'id': 'xyz',
'type': 'Note',
'content': 'foo',
}))
def test_postprocess_as2_hashtag(self):
"""https://github.com/snarfed/bridgy-fed/issues/45"""
self.assert_equals({
'tag': [
{'type': 'Hashtag', 'name': '#bar', 'href': 'bar'},
{'type': 'Hashtag', 'name': '#baz', 'href': 'http://localhost/hashtag/baz'},
{'type': 'Mention', 'href': 'foo'},
],
'to': [as2.PUBLIC_AUDIENCE],
'cc': ['foo'],
2023-06-16 20:16:17 +00:00
}, postprocess_as2({
'tag': [
{'name': 'bar', 'href': 'bar'},
noop, lint fixes from flake8 remaining: $ flake8 --extend-ignore=E501 *.py tests/*.py "pyflakes" failed during execution due to "'FlakesChecker' object has no attribute 'NAMEDEXPR'" Run flake8 with greater verbosity to see more details activitypub.py:15:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused activitypub.py:36:1: F401 'web' imported but unused activitypub.py:48:1: E302 expected 2 blank lines, found 1 activitypub.py:51:9: F811 redefinition of unused 'web' from line 36 app.py:6:1: F401 'flask_app.app' imported but unused app.py:9:1: F401 'activitypub' imported but unused app.py:9:1: F401 'convert' imported but unused app.py:9:1: F401 'follow' imported but unused app.py:9:1: F401 'pages' imported but unused app.py:9:1: F401 'redirect' imported but unused app.py:9:1: F401 'superfeedr' imported but unused app.py:9:1: F401 'ui' imported but unused app.py:9:1: F401 'webfinger' imported but unused app.py:9:1: F401 'web' imported but unused app.py:9:1: F401 'xrpc_actor' imported but unused app.py:9:1: F401 'xrpc_feed' imported but unused app.py:9:1: F401 'xrpc_graph' imported but unused app.py:9:19: E401 multiple imports on one line models.py:19:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused models.py:364:31: E114 indentation is not a multiple of four (comment) models.py:364:31: E116 unexpected indentation (comment) protocol.py:17:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused redirect.py:26:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused web.py:18:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused webfinger.py:13:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused webfinger.py:110:13: E122 continuation line missing indentation or outdented webfinger.py:111:13: E122 continuation line missing indentation or outdented webfinger.py:131:13: E122 continuation line missing indentation or outdented webfinger.py:132:13: E122 continuation line missing indentation or outdented webfinger.py:133:13: E122 continuation line missing indentation or outdented webfinger.py:134:13: E122 continuation line missing indentation or outdented tests/__init__.py:2:1: F401 'oauth_dropins.webutil.tests' imported but unused tests/test_follow.py:11:1: F401 'oauth_dropins.webutil.util.json_dumps' imported but unused tests/test_follow.py:14:1: F401 '.testutil.Fake' imported but unused tests/test_models.py:156:15: E122 continuation line missing indentation or outdented tests/test_models.py:157:15: E122 continuation line missing indentation or outdented tests/test_models.py:158:11: E122 continuation line missing indentation or outdented tests/test_web.py:12:1: F401 'oauth_dropins.webutil.util.json_dumps' imported but unused tests/test_web.py:17:1: F401 '.testutil' imported but unused tests/test_web.py:1513:13: E128 continuation line under-indented for visual indent tests/test_web.py:1514:9: E124 closing bracket does not match visual indentation tests/testutil.py:106:1: E402 module level import not at top of file tests/testutil.py:107:1: E402 module level import not at top of file tests/testutil.py:108:1: E402 module level import not at top of file tests/testutil.py:109:1: E402 module level import not at top of file tests/testutil.py:110:1: E402 module level import not at top of file tests/testutil.py:301:24: E203 whitespace before ':' tests/testutil.py:301:25: E701 multiple statements on one line (colon) tests/testutil.py:301:25: E231 missing whitespace after ':'
2023-06-20 18:22:54 +00:00
{'type': 'Tag', 'name': '#baz'},
# should leave alone
{'type': 'Mention', 'href': 'foo'},
],
}))
def test_postprocess_as2_actor_url_attachments(self):
got = postprocess_as2_actor(as2.from_as1({
'objectType': 'person',
'urls': [
{
'value': 'https://user.com/about-me',
'displayName': 'Mrs. \u2615 Foo',
}, {
'value': 'https://user.com/',
'displayName': 'should be ignored',
}, {
'value': 'http://one',
'displayName': 'one text',
}, {
'value': 'https://two',
'displayName': 'two title',
},
]
}), user=self.user)
self.assert_equals([{
'type': 'PropertyValue',
'name': 'Mrs. ☕ Foo',
'value': '<a rel="me" href="https://user.com/about-me"><span class="invisible">https://</span>user.com/about-me</a>',
}, {
'type': 'PropertyValue',
'name': 'Web site',
'value': '<a rel="me" href="https://user.com"><span class="invisible">https://</span>user.com</a>',
}, {
'type': 'PropertyValue',
'name': 'one text',
'value': '<a rel="me" href="http://one"><span class="invisible">http://</span>one</a>',
}, {
'type': 'PropertyValue',
'name': 'two title',
'value': '<a rel="me" href="https://two"><span class="invisible">https://</span>two</a>',
}], got['attachment'])
def test_postprocess_as2_actor_strips_acct_url(self):
self.assert_equals('http://localhost/r/http://user.com/',
postprocess_as2_actor(as2.from_as1({
'objectType': 'person',
'urls': ['http://user.com/', 'acct:foo@bar'],
}), user=self.user)['url'])
def test_postprocess_as2_actor_preserves_preferredUsername(self):
# preferredUsername stays y.z despite user's username. since Mastodon
# queries Webfinger for preferredUsername@fed.brid.gy
# https://github.com/snarfed/bridgy-fed/issues/77#issuecomment-949955109
self.assertEqual('user.com', postprocess_as2_actor({
'type': 'Person',
'url': 'https://user.com/about-me',
'preferredUsername': 'nick',
'attachment': [{
'type': 'PropertyValue',
'name': 'nick',
'value': '<a rel="me" href="https://user.com/about-me"><span class="invisible">https://</span>user.com/about-me</a>',
}],
}, user=self.user)['preferredUsername'])
def test_postprocess_as2_actor_preferredUsername_is_domain(self):
self.user.has_redirects = True
self.user.put()
self.user.obj.clear()
self.user.obj.as2 = {
'type': 'Person',
'url': ['acct:eve@user.com'],
}
self.user.obj.put()
# preferredUsername stays y.z despite user's username
self.assertEqual('user.com', postprocess_as2_actor({
'type': 'Person',
}, user=self.user)['preferredUsername'])
def test_postprocess_as2_user_wrapped_id(self):
for id in 'http://fed.brid.gy/user.com', 'http://fed.brid.gy/www.user.com':
got = postprocess_as2_actor(as2.from_as1({
'objectType': 'person',
'id': id,
}), user=self.user)
self.assert_equals('http://localhost/user.com', got['id'])
def test_postprocess_as2_mentions_into_cc(self):
obj = copy.deepcopy(MENTION_OBJECT)
del obj['cc']
self.assertEqual(['https://masto.foo/@other'],
postprocess_as2(obj)['cc'])
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
@patch('requests.get')
def test_signed_get_redirects_manually_with_new_sig_headers(self, mock_get):
mock_get.side_effect = [
requests_response(status=302, redirected_url='http://second',
allow_redirects=False),
requests_response(status=200, allow_redirects=False),
]
noop, lint fixes from flake8 remaining: $ flake8 --extend-ignore=E501 *.py tests/*.py "pyflakes" failed during execution due to "'FlakesChecker' object has no attribute 'NAMEDEXPR'" Run flake8 with greater verbosity to see more details activitypub.py:15:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused activitypub.py:36:1: F401 'web' imported but unused activitypub.py:48:1: E302 expected 2 blank lines, found 1 activitypub.py:51:9: F811 redefinition of unused 'web' from line 36 app.py:6:1: F401 'flask_app.app' imported but unused app.py:9:1: F401 'activitypub' imported but unused app.py:9:1: F401 'convert' imported but unused app.py:9:1: F401 'follow' imported but unused app.py:9:1: F401 'pages' imported but unused app.py:9:1: F401 'redirect' imported but unused app.py:9:1: F401 'superfeedr' imported but unused app.py:9:1: F401 'ui' imported but unused app.py:9:1: F401 'webfinger' imported but unused app.py:9:1: F401 'web' imported but unused app.py:9:1: F401 'xrpc_actor' imported but unused app.py:9:1: F401 'xrpc_feed' imported but unused app.py:9:1: F401 'xrpc_graph' imported but unused app.py:9:19: E401 multiple imports on one line models.py:19:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused models.py:364:31: E114 indentation is not a multiple of four (comment) models.py:364:31: E116 unexpected indentation (comment) protocol.py:17:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused redirect.py:26:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused web.py:18:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused webfinger.py:13:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused webfinger.py:110:13: E122 continuation line missing indentation or outdented webfinger.py:111:13: E122 continuation line missing indentation or outdented webfinger.py:131:13: E122 continuation line missing indentation or outdented webfinger.py:132:13: E122 continuation line missing indentation or outdented webfinger.py:133:13: E122 continuation line missing indentation or outdented webfinger.py:134:13: E122 continuation line missing indentation or outdented tests/__init__.py:2:1: F401 'oauth_dropins.webutil.tests' imported but unused tests/test_follow.py:11:1: F401 'oauth_dropins.webutil.util.json_dumps' imported but unused tests/test_follow.py:14:1: F401 '.testutil.Fake' imported but unused tests/test_models.py:156:15: E122 continuation line missing indentation or outdented tests/test_models.py:157:15: E122 continuation line missing indentation or outdented tests/test_models.py:158:11: E122 continuation line missing indentation or outdented tests/test_web.py:12:1: F401 'oauth_dropins.webutil.util.json_dumps' imported but unused tests/test_web.py:17:1: F401 '.testutil' imported but unused tests/test_web.py:1513:13: E128 continuation line under-indented for visual indent tests/test_web.py:1514:9: E124 closing bracket does not match visual indentation tests/testutil.py:106:1: E402 module level import not at top of file tests/testutil.py:107:1: E402 module level import not at top of file tests/testutil.py:108:1: E402 module level import not at top of file tests/testutil.py:109:1: E402 module level import not at top of file tests/testutil.py:110:1: E402 module level import not at top of file tests/testutil.py:301:24: E203 whitespace before ':' tests/testutil.py:301:25: E701 multiple statements on one line (colon) tests/testutil.py:301:25: E231 missing whitespace after ':'
2023-06-20 18:22:54 +00:00
activitypub.signed_get('https://first')
first = mock_get.call_args_list[0][1]
second = mock_get.call_args_list[1][1]
self.assertNotEqual(first['headers'], second['headers'])
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
@patch('requests.get')
def test_signed_get_redirects_to_relative_url(self, mock_get):
mock_get.side_effect = [
# redirected URL is relative, we have to resolve it
requests_response(status=302, redirected_url='/second',
allow_redirects=False),
requests_response(status=200, allow_redirects=False),
]
activitypub.signed_get('https://first')
self.assertEqual(('https://first/second',), mock_get.call_args_list[1][0])
first = mock_get.call_args_list[0][1]
second = mock_get.call_args_list[1][1]
# headers are equal because host is the same
self.assertEqual(first['headers'], second['headers'])
self.assertEqual(
first['auth'].header_signer.sign(first['headers'], method='GET', path='/'),
second['auth'].header_signer.sign(second['headers'], method='GET', path='/'))
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
@patch('requests.post', return_value=requests_response(status=200))
def test_signed_post_from_user_is_activitypub_use_instance_actor(self, mock_post):
activitypub.signed_post('https://url', from_user=ActivityPub(id='http://fed'))
self.assertEqual(1, len(mock_post.call_args_list))
args, kwargs = mock_post.call_args_list[0]
self.assertEqual(('https://url',), args)
rsa_key = kwargs['auth'].header_signer._rsa._key
self.assertEqual(instance_actor().private_pem(), rsa_key.exportKey())
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
@patch('requests.post')
def test_signed_post_ignores_redirect(self, mock_post):
mock_post.side_effect = [
requests_response(status=302, redirected_url='http://second',
allow_redirects=False),
]
resp = activitypub.signed_post('https://first', from_user=self.user)
mock_post.assert_called_once()
self.assertEqual(302, resp.status_code)
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
@patch('requests.get')
def test_fetch_direct(self, mock_get):
mock_get.return_value = AS2
obj = Object(id='http://orig')
ActivityPub.fetch(obj)
self.assertEqual(AS2_OBJ, obj.as2)
mock_get.assert_has_calls((
self.as2_req('http://orig'),
))
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
@patch('requests.get')
def test_fetch_direct_ld_content_type(self, mock_get):
mock_get.return_value = requests_response(AS2_OBJ, headers={
'Content-Type': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
})
obj = Object(id='http://orig')
ActivityPub.fetch(obj)
self.assertEqual(AS2_OBJ, obj.as2)
mock_get.assert_has_calls((
self.as2_req('http://orig'),
))
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
@patch('requests.get')
def test_fetch_via_html(self, mock_get):
mock_get.side_effect = [HTML_WITH_AS2, AS2]
obj = Object(id='http://orig')
ActivityPub.fetch(obj)
self.assertEqual(AS2_OBJ, obj.as2)
mock_get.assert_has_calls((
self.as2_req('http://orig'),
noop, lint fixes from flake8 remaining: $ flake8 --extend-ignore=E501 *.py tests/*.py "pyflakes" failed during execution due to "'FlakesChecker' object has no attribute 'NAMEDEXPR'" Run flake8 with greater verbosity to see more details activitypub.py:15:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused activitypub.py:36:1: F401 'web' imported but unused activitypub.py:48:1: E302 expected 2 blank lines, found 1 activitypub.py:51:9: F811 redefinition of unused 'web' from line 36 app.py:6:1: F401 'flask_app.app' imported but unused app.py:9:1: F401 'activitypub' imported but unused app.py:9:1: F401 'convert' imported but unused app.py:9:1: F401 'follow' imported but unused app.py:9:1: F401 'pages' imported but unused app.py:9:1: F401 'redirect' imported but unused app.py:9:1: F401 'superfeedr' imported but unused app.py:9:1: F401 'ui' imported but unused app.py:9:1: F401 'webfinger' imported but unused app.py:9:1: F401 'web' imported but unused app.py:9:1: F401 'xrpc_actor' imported but unused app.py:9:1: F401 'xrpc_feed' imported but unused app.py:9:1: F401 'xrpc_graph' imported but unused app.py:9:19: E401 multiple imports on one line models.py:19:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused models.py:364:31: E114 indentation is not a multiple of four (comment) models.py:364:31: E116 unexpected indentation (comment) protocol.py:17:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused redirect.py:26:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused web.py:18:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused webfinger.py:13:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused webfinger.py:110:13: E122 continuation line missing indentation or outdented webfinger.py:111:13: E122 continuation line missing indentation or outdented webfinger.py:131:13: E122 continuation line missing indentation or outdented webfinger.py:132:13: E122 continuation line missing indentation or outdented webfinger.py:133:13: E122 continuation line missing indentation or outdented webfinger.py:134:13: E122 continuation line missing indentation or outdented tests/__init__.py:2:1: F401 'oauth_dropins.webutil.tests' imported but unused tests/test_follow.py:11:1: F401 'oauth_dropins.webutil.util.json_dumps' imported but unused tests/test_follow.py:14:1: F401 '.testutil.Fake' imported but unused tests/test_models.py:156:15: E122 continuation line missing indentation or outdented tests/test_models.py:157:15: E122 continuation line missing indentation or outdented tests/test_models.py:158:11: E122 continuation line missing indentation or outdented tests/test_web.py:12:1: F401 'oauth_dropins.webutil.util.json_dumps' imported but unused tests/test_web.py:17:1: F401 '.testutil' imported but unused tests/test_web.py:1513:13: E128 continuation line under-indented for visual indent tests/test_web.py:1514:9: E124 closing bracket does not match visual indentation tests/testutil.py:106:1: E402 module level import not at top of file tests/testutil.py:107:1: E402 module level import not at top of file tests/testutil.py:108:1: E402 module level import not at top of file tests/testutil.py:109:1: E402 module level import not at top of file tests/testutil.py:110:1: E402 module level import not at top of file tests/testutil.py:301:24: E203 whitespace before ':' tests/testutil.py:301:25: E701 multiple statements on one line (colon) tests/testutil.py:301:25: E231 missing whitespace after ':'
2023-06-20 18:22:54 +00:00
self.as2_req('http://as2', headers=as2.CONNEG_HEADERS),
))
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
@patch('requests.get')
def test_fetch_only_html(self, mock_get):
mock_get.return_value = HTML
obj = Object(id='http://orig')
self.assertFalse(ActivityPub.fetch(obj))
self.assertIsNone(obj.as1)
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
@patch('requests.get')
def test_fetch_not_acceptable(self, mock_get):
noop, lint fixes from flake8 remaining: $ flake8 --extend-ignore=E501 *.py tests/*.py "pyflakes" failed during execution due to "'FlakesChecker' object has no attribute 'NAMEDEXPR'" Run flake8 with greater verbosity to see more details activitypub.py:15:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused activitypub.py:36:1: F401 'web' imported but unused activitypub.py:48:1: E302 expected 2 blank lines, found 1 activitypub.py:51:9: F811 redefinition of unused 'web' from line 36 app.py:6:1: F401 'flask_app.app' imported but unused app.py:9:1: F401 'activitypub' imported but unused app.py:9:1: F401 'convert' imported but unused app.py:9:1: F401 'follow' imported but unused app.py:9:1: F401 'pages' imported but unused app.py:9:1: F401 'redirect' imported but unused app.py:9:1: F401 'superfeedr' imported but unused app.py:9:1: F401 'ui' imported but unused app.py:9:1: F401 'webfinger' imported but unused app.py:9:1: F401 'web' imported but unused app.py:9:1: F401 'xrpc_actor' imported but unused app.py:9:1: F401 'xrpc_feed' imported but unused app.py:9:1: F401 'xrpc_graph' imported but unused app.py:9:19: E401 multiple imports on one line models.py:19:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused models.py:364:31: E114 indentation is not a multiple of four (comment) models.py:364:31: E116 unexpected indentation (comment) protocol.py:17:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused redirect.py:26:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused web.py:18:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused webfinger.py:13:1: F401 'oauth_dropins.webutil.util.json_loads' imported but unused webfinger.py:110:13: E122 continuation line missing indentation or outdented webfinger.py:111:13: E122 continuation line missing indentation or outdented webfinger.py:131:13: E122 continuation line missing indentation or outdented webfinger.py:132:13: E122 continuation line missing indentation or outdented webfinger.py:133:13: E122 continuation line missing indentation or outdented webfinger.py:134:13: E122 continuation line missing indentation or outdented tests/__init__.py:2:1: F401 'oauth_dropins.webutil.tests' imported but unused tests/test_follow.py:11:1: F401 'oauth_dropins.webutil.util.json_dumps' imported but unused tests/test_follow.py:14:1: F401 '.testutil.Fake' imported but unused tests/test_models.py:156:15: E122 continuation line missing indentation or outdented tests/test_models.py:157:15: E122 continuation line missing indentation or outdented tests/test_models.py:158:11: E122 continuation line missing indentation or outdented tests/test_web.py:12:1: F401 'oauth_dropins.webutil.util.json_dumps' imported but unused tests/test_web.py:17:1: F401 '.testutil' imported but unused tests/test_web.py:1513:13: E128 continuation line under-indented for visual indent tests/test_web.py:1514:9: E124 closing bracket does not match visual indentation tests/testutil.py:106:1: E402 module level import not at top of file tests/testutil.py:107:1: E402 module level import not at top of file tests/testutil.py:108:1: E402 module level import not at top of file tests/testutil.py:109:1: E402 module level import not at top of file tests/testutil.py:110:1: E402 module level import not at top of file tests/testutil.py:301:24: E203 whitespace before ':' tests/testutil.py:301:25: E701 multiple statements on one line (colon) tests/testutil.py:301:25: E231 missing whitespace after ':'
2023-06-20 18:22:54 +00:00
mock_get.return_value = NOT_ACCEPTABLE
obj = Object(id='http://orig')
self.assertFalse(ActivityPub.fetch(obj))
self.assertIsNone(obj.as1)
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
@patch('requests.get')
def test_fetch_ssl_error(self, mock_get):
mock_get.side_effect = requests.exceptions.SSLError
with self.assertRaises(BadGateway):
ActivityPub.fetch(Object(id='http://orig'))
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
@patch('requests.get')
def test_fetch_no_content(self, mock_get):
mock_get.return_value = self.as2_resp('')
with self.assertRaises(BadGateway):
ActivityPub.fetch(Object(id='http://the/id'))
mock_get.assert_has_calls([self.as2_req('http://the/id')])
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
@patch('requests.get')
def test_fetch_not_json(self, mock_get):
mock_get.return_value = self.as2_resp('XYZ not JSON')
with self.assertRaises(BadGateway):
ActivityPub.fetch(Object(id='http://the/id'))
mock_get.assert_has_calls([self.as2_req('http://the/id')])
def test_fetch_non_url(self):
obj = Object(id='x y z')
self.assertFalse(ActivityPub.fetch(obj))
self.assertIsNone(obj.as1)
def test_convert(self):
obj = Object()
self.assertEqual({}, ActivityPub.convert(obj))
obj.our_as1 = {}
self.assertEqual({}, ActivityPub.convert(obj))
2023-11-03 22:11:21 +00:00
obj.as2 = {'baz': 'biff'}
self.assert_equals({'baz': 'biff'}, ActivityPub.convert(obj))
2023-11-03 22:11:21 +00:00
# prevent HTTP fetch to infer protocol
self.store_object(id='https://mas.to/thing', source_protocol='activitypub')
obj.as2 = None
obj.our_as1 = {
'id': 'fake:like',
'objectType': 'activity',
'verb': 'like',
2023-11-03 22:11:21 +00:00
'actor': 'fake:user',
'object': 'https://mas.to/thing',
}
self.assertEqual({
'@context': 'https://www.w3.org/ns/activitystreams',
2023-11-03 22:11:21 +00:00
'id': 'https://fa.brid.gy/convert/ap/fake:like',
'type': 'Like',
2023-11-03 22:11:21 +00:00
'actor': 'https://fa.brid.gy/ap/fake:user',
'object': 'https://mas.to/thing',
'to': [as2.PUBLIC_AUDIENCE],
}, ActivityPub.convert(obj))
def test_convert_actor_as2(self):
self.assert_equals(ACTOR, ActivityPub.convert(Object(as2=ACTOR)))
@patch('requests.get')
def test_convert_actor_as1_from_user(self, mock_get):
mock_get.return_value = requests_response(test_web.ACTOR_HTML)
obj = Object(our_as1={
'objectType': 'person',
'id': 'https://user.com/',
})
self.assert_equals(ACTOR_BASE, ActivityPub.convert(obj, from_user=self.user),
ignore=['endpoints', 'followers', 'following'])
def test_convert_actor_as1_no_from_user(self):
obj = Object(our_as1=ACTOR_AS1)
self.assert_equals(ACTOR, common.unwrap(ActivityPub.convert(obj)),
ignore=['to', 'attachment'])
def test_convert_follow_as1_no_from_user(self):
# prevent HTTP fetches to infer protocol
self.store_object(id='https://mas.to/6d1a', source_protocol='activitypub')
self.store_object(id='https://user.com/', source_protocol='web')
obj = Object(our_as1=as2.to_as1(FOLLOW))
self.assert_equals(FOLLOW, common.unwrap(ActivityPub.convert(obj)),
ignore=['to'])
def test_convert_profile_update_as1_no_from_user(self):
obj = Object(our_as1={
'objectType': 'activity',
'verb': 'update',
'object': ACTOR_AS1,
})
self.assert_equals({
'type': 'Update',
'object': ACTOR,
}, common.unwrap(ActivityPub.convert(obj)), ignore=['to', 'attachment'])
def test_convert_compact_actor_attributedTo_author(self):
obj = Object(our_as1={
'actor': {'id': 'baj'},
'author': [{'id': 'bar'}],
'object': {'author': {'id': 'biff'}},
})
self.assert_equals({
'actor': 'baj',
'attributedTo': 'bar',
'object': {'attributedTo': 'biff'},
}, ActivityPub.convert(obj), ignore=['to'])
def test_convert_adds_context_to_as2(self):
obj = Object(as2={
'type': 'Update',
'object': ACTOR,
})
# use assertEquals so that we don't ignore @context
self.assertEqual({
'@context': [as2.CONTEXT, activitypub.SECURITY_CONTEXT],
'type': 'Update',
'object': ACTOR,
}, ActivityPub.convert(obj))
# TODO: remove
@skip
def test_convert_protocols_not_enabled(self):
obj = Object(our_as1={'foo': 'bar'}, source_protocol='atproto')
with self.assertRaises(BadRequest):
ActivityPub.convert(obj)
def test_postprocess_as2_idempotent(self):
for obj in (ACTOR, REPLY_OBJECT, REPLY_OBJECT_WRAPPED, REPLY,
NOTE_OBJECT, NOTE, MENTION_OBJECT, MENTION, LIKE,
LIKE_WRAPPED, REPOST, FOLLOW, FOLLOW_WRAPPED, ACCEPT,
UNDO_FOLLOW_WRAPPED, DELETE, UPDATE_NOTE,
LIKE_WITH_ACTOR, REPOST_FULL, FOLLOW_WITH_ACTOR,
FOLLOW_WRAPPED_WITH_ACTOR, FOLLOW_WITH_OBJECT, UPDATE_PERSON,
):
with self.subTest(obj=obj):
obj = copy.deepcopy(obj)
2023-06-16 20:16:17 +00:00
self.assert_equals(postprocess_as2(obj),
postprocess_as2(postprocess_as2(obj)),
ignore=['to'])
def test_handle_as(self):
user = ActivityPub(obj=Object(id='a', as2={
**ACTOR,
'preferredUsername': 'me',
}))
self.assertEqual('@me@mas.to', user.handle_as(ActivityPub))
self.assertEqual('@me@mas.to', user.handle)
user.obj.as2 = ACTOR
self.assertEqual('@swentel@mas.to', user.handle_as(ActivityPub))
self.assertEqual('@swentel@mas.to', user.handle)
user = ActivityPub(id='https://mas.to/users/alice')
self.assertEqual('@alice@mas.to', user.handle_as(ActivityPub))
self.assertEqual('@alice@mas.to', user.handle)
user = self.make_user('http://a', cls=ActivityPub, obj_as2={
'id': 'https://mas.to/users/foo',
'preferredUsername': 'me',
})
self.assertEqual('me.mas.to.ap.brid.gy', user.handle_as('atproto'))
def test_web_url_composite_url_object(self):
actor_as2 = {
'type': 'Person',
'url': 'https://techhub.social/@foo',
'attachment': [{
'type': 'PropertyValue',
'name': 'Twitter',
'value': '<span class="h-card"><a href="https://techhub.social/@foo" class="u-url mention">@<span>foo</span></a></span>',
}],
}
user = self.make_user('http://foo/actor', cls=ActivityPub, obj_as2=actor_as2)
self.assertEqual('https://techhub.social/@foo', user.web_url())
def test_web_url(self):
user = self.make_user('http://foo/actor', cls=ActivityPub)
self.assertEqual('http://foo/actor', user.web_url())
2023-07-10 19:23:00 +00:00
user.obj = Object(id='a', as2=copy.deepcopy(ACTOR)) # no url
self.assertEqual('http://foo/actor', user.web_url())
user.obj.as2['url'] = ['http://my/url']
self.assertEqual('http://my/url', user.web_url())
def test_handle(self):
user = self.make_user('http://foo/ey', cls=ActivityPub)
self.assertIsNone(user.handle)
self.assertEqual('http://foo/ey', user.handle_or_id())
user.obj = Object(id='a', as2=ACTOR)
self.assertEqual('@swentel@mas.to', user.handle)
self.assertEqual('@swentel@mas.to', user.handle_or_id())
2023-06-16 20:16:17 +00:00
@skip
def test_target_for_not_activitypub(self):
2023-06-16 20:16:17 +00:00
with self.assertRaises(AssertionError):
ActivityPub.target_for(Object(source_protocol='web'))
def test_target_for_actor(self):
2023-06-16 20:16:17 +00:00
self.assertEqual(ACTOR['inbox'], ActivityPub.target_for(
Object(source_protocol='ap', as2=ACTOR)))
actor = copy.deepcopy(ACTOR)
del actor['inbox']
self.assertIsNone(ActivityPub.target_for(
Object(source_protocol='ap', as2=actor)))
actor['publicInbox'] = 'so-public'
self.assertEqual('so-public', ActivityPub.target_for(
Object(source_protocol='ap', as2=actor)))
# sharedInbox
self.assertEqual('so-public', ActivityPub.target_for(
Object(source_protocol='ap', as2=actor), shared=True))
actor['endpoints'] = {
'sharedInbox': 'so-shared',
}
self.assertEqual('so-public', ActivityPub.target_for(
Object(source_protocol='ap', as2=actor)))
self.assertEqual('so-shared', ActivityPub.target_for(
Object(source_protocol='ap', as2=actor), shared=True))
def test_target_for_object(self):
obj = Object(as2=NOTE_OBJECT, source_protocol='ap')
self.assertIsNone(ActivityPub.target_for(obj))
Object(id=ACTOR['id'], as2=ACTOR).put()
obj.as2 = {
**NOTE_OBJECT,
'author': ACTOR['id'],
}
self.assertEqual('http://mas.to/inbox', ActivityPub.target_for(obj))
del obj.as2['author']
obj.as2['actor'] = copy.deepcopy(ACTOR)
obj.as2['actor']['url'] = [obj.as2['actor'].pop('id')]
self.assertEqual('http://mas.to/inbox', ActivityPub.target_for(obj))
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
@patch('requests.get')
def test_target_for_object_fetch(self, mock_get):
mock_get.return_value = self.as2_resp(ACTOR)
obj = Object(as2={
**NOTE_OBJECT,
'author': 'http://the/author',
}, source_protocol='ap')
self.assertEqual('http://mas.to/inbox', ActivityPub.target_for(obj))
mock_get.assert_has_calls([self.as2_req('http://the/author')])
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
@patch('requests.get')
def test_target_for_author_is_object_id(self, mock_get):
2023-11-03 22:11:21 +00:00
mock_get.return_value = HTML
obj = self.store_object(id='http://the/author', our_as1={
'author': 'http://the/author',
})
# test is that we short circuit out instead of infinite recursion
self.assertIsNone(ActivityPub.target_for(obj))
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
@patch('requests.post')
def test_send_blocklisted(self, mock_post):
self.assertFalse(ActivityPub.send(Object(as2=NOTE),
'https://fed.brid.gy/ap/sharedInbox'))
mock_post.assert_not_called()
2023-11-03 22:11:21 +00:00
Revert "cache outbound HTTP request responses, locally to each inbound request" This reverts commit 30debfc8faf730190bd51a3aef49df6c6bfbd50a. seemed promising, but broke in production. Saw a lot of `IncompleteRead`s on both GETs and POSTs. Rolled back for now. ``` ('Connection broken: IncompleteRead(9172 bytes read, -4586 more expected)', IncompleteRead(9172 bytes read, -4586 more expected)) ... File "oauth_dropins/webutil/util.py", line 1673, in call resp = getattr((session or requests), fn)(url, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 102, in get return self.request('GET', url, params=params, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 158, in request return super().request(method, url, *args, headers=headers, **kwargs) # type: ignore ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/sessions.py", line 589, in request resp = self.send(prep, **send_kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 205, in send response = self._send_and_cache(request, actions, cached_response, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/session.py", line 233, in _send_and_cache self.cache.save_response(response, actions.cache_key, actions.expires) File "requests_cache/backends/base.py", line 89, in save_response cached_response = CachedResponse.from_response(response, expires=expires) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/response.py", line 102, in from_response obj.raw = CachedHTTPResponse.from_response(response) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests_cache/models/raw_response.py", line 69, in from_response _ = response.content # This property reads, decodes, and stores response content ^^^^^^^^^^^^^^^^ File "requests/models.py", line 899, in content self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "requests/models.py", line 818, in generate raise ChunkedEncodingError(e) ```
2024-03-08 21:24:28 +00:00
@patch('requests.post')
2023-11-03 22:11:21 +00:00
def test_send_convert_ids(self, mock_post):
mock_post.return_value = requests_response()
like = Object(our_as1={
'id': 'fake:like',
'objectType': 'activity',
'verb': 'like',
'object': 'fake:post',
'actor': 'fake:user',
})
self.assertTrue(ActivityPub.send(like, 'https://inbox', from_user=self.user))
2023-11-03 22:11:21 +00:00
self.assertEqual(1, len(mock_post.call_args_list))
args, kwargs = mock_post.call_args_list[0]
self.assertEqual(('https://inbox',), args)
self.assertEqual({
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'https://fa.brid.gy/convert/ap/fake:like',
'type': 'Like',
'object': 'https://fa.brid.gy/convert/ap/fake:post',
'actor': 'https://fa.brid.gy/ap/fake:user',
'to': [as2.PUBLIC_AUDIENCE],
}, json_loads(kwargs['data']))