bridgy-fed/tests/test_pages.py

832 wiersze
32 KiB
Python
Czysty Zwykły widok Historia

"""Unit tests for pages.py."""
from unittest import skip
from unittest.mock import patch
import arroba.server
import copy
from flask import get_flashed_messages, session
from google.cloud import ndb
from google.cloud.tasks_v2.types import Task
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 granary import atom, microformats2, rss
from oauth_dropins.bluesky import BlueskyAuth
from oauth_dropins.mastodon import MastodonAuth
from oauth_dropins.views import LOGINS_SESSION_KEY
from oauth_dropins.webutil import util
from oauth_dropins.webutil.appengine_config import tasks_client
from oauth_dropins.webutil.testutil import requests_response
from requests import ConnectionError
# import first so that Fake is defined before URL routes are registered
from .testutil import (
Fake,
ExplicitFake,
OtherFake,
TestCase,
ACTOR,
COMMENT,
MENTION,
NOTE,
)
from activitypub import ActivityPub
from atproto import ATProto
import common
from models import Object, Follower, Target
from web import Web
from granary.tests.test_bluesky import ACTOR_AS, ACTOR_PROFILE_BSKY
from .test_atproto import DID_DOC
from .test_web import (
ACTOR_AS1_UNWRAPPED_URLS,
ACTOR_AS2,
ACTOR_HTML,
ACTOR_HTML_RESP,
ACTOR_MF2_REL_URLS,
REPOST_AS2,
)
ACTOR_WITH_PREFERRED_USERNAME = {
'objectType': 'person',
'displayName': 'Me',
'username': 'me',
'url': 'https://plus.google.com/bob',
'image': 'http://pic',
}
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
def contents(activities):
return [
' '.join(util.parse_html((a.get('object') or a)['content']).get_text().split())
for a in activities]
class PagesTest(TestCase):
EXPECTED = contents([COMMENT, MENTION, NOTE])
EXPECTED_SNIPPETS = [
'Dr. Eve replied a comment',
'tag:fake.com:44... posted a mention',
'🌐 user.com posted my note',
]
def setUp(self):
super().setUp()
self.user = self.make_user('user.com', cls=Web, has_redirects=True)
def test_user(self):
got = self.client.get('/web/user.com', base_url='https://fed.brid.gy/')
self.assert_equals(200, got.status_code)
def test_user_fake(self):
self.make_user('fake:foo', cls=Fake)
got = self.client.get('/fake/fake:foo')
self.assert_equals(200, got.status_code)
def test_user_page_handle_activitypub(self):
user = self.make_user('http://fo/o', cls=ActivityPub,
enabled_protocols=['fake'],
obj_as1=ACTOR_WITH_PREFERRED_USERNAME)
self.assertEqual('@me@fo', user.handle_as(ActivityPub))
got = self.client.get('/ap/@me@fo')
self.assert_equals(200, got.status_code)
# TODO: can't handle slashes in id segment of path. is that ok?
# got = self.client.get('/ap/http%3A//foo')
# self.assert_equals(302, got.status_code)
# self.assert_equals('/ap/@me@plus.google.com', got.headers['Location'])
body = got.get_data(as_text=True)
self.assertIn('/ap/@me@fo/update-profile', body)
self.assertNotIn('/ap/http://fo/o/update-profile', body)
def test_user_page_atproto_id(self):
self.store_object(id='did:plc:user', raw={
**DID_DOC,
'alsoKnownAs': ['at://han.dull'],
})
user = self.make_user('did:plc:user', cls=ATProto, enabled_protocols=['fake'])
got = self.client.get('/bsky/@han.dull')
self.assert_equals(200, got.status_code)
got = self.client.get('/bsky/han.dull')
self.assert_equals(200, got.status_code)
body = got.get_data(as_text=True)
self.assertIn('/bsky/did:plc:user/update-profile', body)
self.assertNotIn('/bsky/han.dull/update-profile', body)
def test_user_page_handle_atproto_avoid_old_bad_user(self):
# first user has computed handle property stored as go.od, but DID doc has
# it updated to b.ad instead
did = self.store_object(id='did:plc:1', raw={
**DID_DOC,
'alsoKnownAs': ['at://go.od'],
})
user_1 = self.make_user('did:plc:1', cls=ATProto, enabled_protocols=['fake'])
did.raw['alsoKnownAs'] = ['at://b.ad']
did.put()
# second DID's handle is go.od, its DID is after did:plc:1 so queries return it
self.store_object(id='did:plc:2', raw={
**DID_DOC,
'alsoKnownAs': ['at://go.od'],
})
user_2 = self.make_user('did:plc:2', cls=ATProto, enabled_protocols=['fake'])
self.assertEqual([user_1, user_2],
ATProto.query(ATProto.handle == 'go.od').fetch())
got = self.client.get('/bsky/go.od')
self.assert_equals(200, got.status_code)
html = got.get_data(as_text=True)
self.assertIn('did:plc:2', html)
self.assertNotIn('did:plc:1', html)
def test_user_page_enabled_activitypub_rel_alternate(self):
user = self.make_user('fake:user', cls=Fake, enabled_protocols=['activitypub'])
got = self.client.get('/fake/fake:user')
self.assert_equals(200, got.status_code)
self.assertIn(
'<link href="https://fa.brid.gy/ap/fake:user" rel="alternate" type="application/activity+json">',
got.get_data(as_text=True))
def test_user_web_custom_username_doesnt_redirect(self):
"""https://github.com/snarfed/bridgy-fed/issues/534"""
self.user.obj = Object(id='a', as2={
**ACTOR_AS2,
'url': 'acct:baz@user.com',
})
self.user.obj.put()
self.user.put()
self.assertEqual('baz', self.user.username())
got = self.client.get('/web/@baz@user.com')
self.assert_equals(404, got.status_code)
got = self.client.get('/web/baz')
self.assert_equals(404, got.status_code)
got = self.client.get('/web/user.com')
self.assert_equals(200, got.status_code)
self.assertIn('@baz@user.com', got.get_data(as_text=True))
def test_user_www_domain_special_case(self):
"""https://github.com/snarfed/bridgy-fed/issues/1244"""
www = self.make_user('www.jvt.me', cls=Web)
got = self.client.get('/web/www.jvt.me')
self.assert_equals(200, got.status_code)
def test_user_objects(self):
self.add_objects()
got = self.client.get('/web/user.com')
self.assert_equals(200, got.status_code)
def test_user_not_found(self):
got = self.client.get('/web/bar.com')
self.assert_equals(404, got.status_code)
def test_user_opted_out(self):
self.user.obj.our_as1 = {'summary': '#nobridge'}
self.user.obj.put()
got = self.client.get('/web/user.com')
self.assert_equals(404, got.status_code)
def test_user_handle_opted_out(self):
user = self.make_user('fake:user', cls=Fake, manual_opt_out=True)
got = self.client.get('/fa/fake:handle:user')
self.assert_equals(404, got.status_code)
def test_user_default_serve_false_no_enabled_protocols(self):
self.make_user('other:foo', cls=OtherFake)
got = self.client.get('/other/other:foo')
self.assert_equals(404, got.status_code)
def test_user_default_serve_false_enabled_protocols(self):
self.make_user('other:foo', cls=OtherFake, enabled_protocols=['fake'])
got = self.client.get('/other/other:foo')
self.assert_equals(200, got.status_code)
def test_user_use_instead(self):
self.make_user('bar.com', cls=Web, use_instead=self.user.key)
got = self.client.get('/web/bar.com')
self.assert_equals(302, got.status_code)
self.assert_equals('/web/user.com', got.headers['Location'])
got = self.client.get('/web/user.com')
self.assert_equals(200, got.status_code)
def test_user_object_bare_string_id(self):
2025-01-31 22:57:56 +00:00
Object(id='a', users=[self.user.key], as2=REPOST_AS2).put()
got = self.client.get('/web/user.com')
self.assert_equals(200, got.status_code)
def test_user_object_url_object(self):
with self.request_context:
2025-01-31 22:57:56 +00:00
Object(id='a', users=[self.user.key], our_as1={
**REPOST_AS2,
'object': {
'id': 'https://mas.to/toot/id',
'url': {'value': 'http://foo', 'displayName': 'bar'},
},
}).put()
got = self.client.get('/web/user.com')
self.assert_equals(200, got.status_code)
def test_user_before(self):
self.add_objects()
got = self.client.get(f'/web/user.com?before={util.now().isoformat()}')
self.assert_equals(200, got.status_code)
def test_user_after(self):
self.add_objects()
got = self.client.get(f'/web/user.com?after={util.now().isoformat()}')
self.assert_equals(200, got.status_code)
def test_user_before_bad(self):
self.add_objects()
got = self.client.get('/web/user.com?before=nope')
self.assert_equals(400, got.status_code)
def test_user_before_and_after(self):
self.add_objects()
got = self.client.get('/web/user.com?before=2024-01-01+01:01:01&after=2023-01-01+01:01:01')
self.assert_equals(400, got.status_code)
def test_user_protocol_bot_user(self):
bot = self.make_user(id='fa.brid.gy', cls=Web)
got = self.client.get(f'/web/fa.brid.gy')
self.assert_equals(404, got.status_code)
def test_update_profile(self):
user = self.make_user('fake:user', cls=Fake)
user.obj.copies = [Target(protocol='other', uri='other:profile:fake:user')]
user.obj.put()
bob = self.make_user('other:bob', cls=OtherFake)
Follower.get_or_create(to=user, from_=bob)
actor = {
'objectType': 'person',
'id': 'fake:profile:user',
'displayName': 'Ms User',
}
Fake.fetchable = {'fake:profile:user': actor}
got = self.client.post('/fa/fake:user/update-profile')
self.assert_equals(302, got.status_code)
self.assert_equals('/fa/fake:handle:user', got.headers['Location'])
2024-05-29 23:46:16 +00:00
self.assertEqual(
['Updating profile from <a href="web:fake:user">fake:handle:user</a>...'],
get_flashed_messages())
self.assertEqual(['fake:profile:user'], Fake.fetched)
actor['updated'] = '2022-01-02T03:04:05+00:00'
self.assert_object('fake:profile:user', source_protocol='fake', our_as1=actor,
users=[user.key], copies=user.obj.copies)
update = {
'objectType': 'activity',
'verb': 'update',
'id': 'fake:profile:user#bridgy-fed-update-2022-01-02T03:04:05+00:00',
'actor': {**actor, 'id': 'fake:user'},
'object': actor,
}
self.assertEqual([('other:bob:target', update)], OtherFake.sent)
@patch.object(Fake, 'fetch', side_effect=ConnectionError('foo'))
def test_update_profile_load_fails(self, _):
self.make_user('fake:user', cls=Fake)
got = self.client.post('/fa/fake:user/update-profile')
self.assert_equals(302, got.status_code)
self.assert_equals('/fa/fake:handle:user', got.headers['Location'])
self.assertEqual(
2024-05-29 23:46:16 +00:00
['Couldn\'t update profile for <a href="web:fake:user">fake:handle:user</a>: foo'],
get_flashed_messages())
@patch.object(tasks_client, 'create_task', return_value=Task(name='my task'))
def test_update_profile_receive_task(self, mock_create_task):
common.RUN_TASKS_INLINE = False
user = self.make_user('fake:user', cls=Fake)
Fake.fetchable = {'fake:profile:user': {
'objectType': 'person',
'id': 'fake:user',
'displayName': 'Ms User',
}}
# use handle in URL to check that we use key id as authed_as below
got = self.client.post('/fa/fake:handle:user/update-profile')
self.assert_equals(302, got.status_code)
self.assert_equals('/fa/fake:handle:user', got.headers['Location'])
self.assert_equals(['fake:profile:user'], Fake.fetched)
self.assert_task(mock_create_task, 'receive', obj_id='fake:profile:user',
authed_as='fake:user')
@patch('requests.get', return_value=ACTOR_HTML_RESP)
def test_update_profile_web(self, mock_get):
self.user.obj.copies = [
Target(protocol='fake', uri='fa:profile:web:user.com'),
Target(protocol='other', uri='other:profile:web:user.com'),
]
self.user.enabled_protocols = ['other']
self.user.obj.put()
Follower.get_or_create(from_=self.make_user('fake:user', cls=Fake),
to=self.user)
got = self.client.post('/web/user.com/update-profile')
self.assert_equals(302, got.status_code)
self.assert_equals('/web/user.com', got.headers['Location'])
user = self.user.key.get()
self.assertIsNone(user.status)
expected_mf2 = copy.deepcopy(ACTOR_MF2_REL_URLS)
expected_mf2['rel-urls']['https://user.com/webmention'] = {
'rels': ['webmention'],
'text': '',
}
self.assertEqual(expected_mf2, user.obj.mf2)
actor_as1 = {
**ACTOR_AS1_UNWRAPPED_URLS,
'updated': '2022-01-02T03:04:05+00:00',
}
self.assertEqual([('fake:shared:target', {
'objectType': 'activity',
'verb': 'update',
'id': 'https://user.com/#bridgy-fed-update-2022-01-02T03:04:05+00:00',
'actor': {**actor_as1, 'id': 'user.com'},
'object': actor_as1,
})], Fake.sent)
self.assertEqual({'user.com': 'user.com'}, OtherFake.usernames)
@patch('requests.get', return_value=requests_response(
ACTOR_HTML, url='https://www.user.com/'))
def test_update_profile_web_www(self, mock_get):
self.user.obj.copies = [
Target(protocol='fake', uri='fa:profile:web:user.com'),
]
self.user.obj.put()
Follower.get_or_create(from_=self.make_user('fake:user', cls=Fake),
to=self.user)
got = self.client.post('/web/user.com/update-profile')
self.assert_equals(302, got.status_code)
self.assert_equals('/web/user.com', got.headers['Location'])
actor_as1 = {
**ACTOR_AS1_UNWRAPPED_URLS,
'updated': '2022-01-02T03:04:05+00:00',
'urls': [
{'value': 'https://user.com/', 'displayName': 'Ms. ☕ Baz'},
{'value': 'https://www.user.com/'},
],
}
self.assertEqual([('fake:shared:target', {
'objectType': 'activity',
'verb': 'update',
'id': 'https://user.com/#bridgy-fed-update-2022-01-02T03:04:05+00:00',
'actor': {**actor_as1, 'id': 'user.com'},
'object': actor_as1,
})], Fake.sent)
@patch('requests.get', return_value=requests_response(
ACTOR_HTML.replace('Ms. ☕ Baz', 'Ms. ☕ Baz #nobridge'),
url='https://user.com/'))
def test_update_profile_web_delete(self, mock_get):
self.user.obj.copies = [Target(protocol='fake', uri='fa:profile:web:user.com')]
self.user.obj.put()
Follower.get_or_create(from_=self.make_user('fake:user', cls=Fake),
to=self.user)
got = self.client.post('/web/user.com/update-profile')
self.assert_equals(302, got.status_code)
self.assert_equals('/web/user.com', got.headers['Location'])
user = self.user.key.get()
self.assertEqual('nobridge', user.status)
self.assertEqual([('fake:shared:target', {
'objectType': 'activity',
'verb': 'delete',
'id': 'https://user.com/#bridgy-fed-delete-user-all-2022-01-02T03:04:05+00:00',
'actor': 'user.com',
'object': 'user.com',
})], Fake.sent)
def test_followers(self):
Follower.get_or_create(
to=self.user,
from_=self.make_user('http://un/used', cls=ActivityPub, obj_as1={
**ACTOR,
'id': 'http://un/used',
'url': 'http://stored/users/follow',
}))
Follower.get_or_create(
to=self.user,
from_=self.make_user('http://masto/user', cls=ActivityPub,
obj_as1=ACTOR_WITH_PREFERRED_USERNAME))
from models import PROTOCOLS
got = self.client.get('/web/user.com/followers')
self.assert_equals(200, got.status_code)
body = got.get_data(as_text=True)
self.assertIn('@follow@stored', body)
self.assertIn('@me@masto', body)
def test_home_fake(self):
self.make_user('fake:foo', cls=Fake)
got = self.client.get('/fake/fake:foo/home')
self.assert_equals(200, got.status_code)
def test_home_objects(self):
self.add_objects()
got = self.client.get('/web/user.com/home')
self.assert_equals(200, got.status_code)
def test_notifications_fake(self):
self.make_user('fake:foo', cls=Fake)
got = self.client.get('/fake/fake:foo/notifications')
self.assert_equals(200, got.status_code)
def test_notifications_objects(self):
self.add_objects()
got = self.client.get('/web/user.com/notifications')
self.assert_equals(200, got.status_code)
def test_notifications_rss(self):
self.add_objects()
got = self.client.get('/web/user.com/notifications?format=rss')
self.assert_equals(200, got.status_code)
self.assert_equals(rss.CONTENT_TYPE, got.headers['Content-Type'])
self.assert_equals(self.EXPECTED_SNIPPETS,
contents(rss.to_activities(got.text)))
def test_notifications_atom(self):
self.add_objects()
got = self.client.get('/web/user.com/notifications?format=atom')
self.assert_equals(200, got.status_code)
self.assert_equals(atom.CONTENT_TYPE, got.headers['Content-Type'])
self.assert_equals(self.EXPECTED_SNIPPETS,
contents(atom.atom_to_activities(got.text)))
def test_notifications_html(self):
self.add_objects()
got = self.client.get('/web/user.com/notifications?format=html')
self.assert_equals(200, got.status_code)
self.assert_equals(self.EXPECTED_SNIPPETS,
contents(microformats2.html_to_activities(got.text)))
def test_followers_fake(self):
self.make_user('fake:foo', cls=Fake)
got = self.client.get('/fake/fake:foo/followers')
self.assert_equals(200, got.status_code)
def test_followers_activitypub(self):
user = self.make_user('https://inst/user', cls=ActivityPub,
enabled_protocols=['fake'],
obj_as1=ACTOR_WITH_PREFERRED_USERNAME)
got = self.client.get('/ap/@me@inst/followers')
self.assert_equals(200, got.status_code)
self.assert_equals('text/html', got.headers['Content-Type'].split(';')[0])
def test_followers_empty(self):
got = self.client.get('/web/user.com/followers')
self.assert_equals(200, got.status_code)
self.assertNotIn('class="follower', got.get_data(as_text=True))
def test_followers_user_not_found(self):
got = self.client.get('/web/nope.com/followers')
self.assert_equals(404, got.status_code)
def test_following(self):
Follower.get_or_create(
from_=self.user,
to=self.make_user('http://un/used', cls=ActivityPub, obj_as1={
**ACTOR,
'id': 'http://un/used',
'url': 'http://stored/users/follow',
}))
Follower.get_or_create(
from_=self.user,
to=self.make_user('http://masto/user', cls=ActivityPub,
obj_as1=ACTOR_WITH_PREFERRED_USERNAME))
got = self.client.get('/web/user.com/following')
self.assert_equals(200, got.status_code)
body = got.get_data(as_text=True)
self.assertIn('@follow@stored', body)
self.assertIn('@me@masto', body)
def test_following_empty(self):
got = self.client.get('/web/user.com/following')
self.assert_equals(200, got.status_code)
self.assertNotIn('class="follower', got.get_data(as_text=True))
def test_following_fake(self):
self.make_user('fake:foo', cls=Fake)
got = self.client.get('/fake/fake:foo/following')
self.assert_equals(200, got.status_code)
def test_following_user_not_found(self):
got = self.client.get('/web/nope.com/following')
self.assert_equals(404, got.status_code)
def test_following_before_empty(self):
got = self.client.get(f'/web/user.com/following?before={util.now().isoformat()}')
self.assert_equals(200, got.status_code)
def test_following_after_empty(self):
got = self.client.get(f'/web/user.com/following?after={util.now().isoformat()}')
self.assert_equals(200, got.status_code)
def test_feed_user_not_found(self):
got = self.client.get('/web/nope.com/feed')
self.assert_equals(404, got.status_code)
def test_feed_fake(self):
self.make_user('fake:foo', cls=Fake)
got = self.client.get('/fake/fake:foo/feed')
self.assert_equals(200, got.status_code)
def test_feed_html_empty(self):
got = self.client.get('/web/user.com/feed')
self.assert_equals(200, got.status_code)
self.assert_equals([], microformats2.html_to_activities(got.text))
def test_feed_html(self):
self.add_objects()
# repost with object (original post) in separate Object
repost = {
'objectType': 'activity',
'verb': 'share',
'object': 'fake:orig',
}
orig = {
'objectType': 'note',
'content': 'biff',
}
2023-07-28 22:49:29 +00:00
self.store_object(id='fake:repost', feed=[self.user.key], our_as1=repost)
self.store_object(id='fake:orig', our_as1=orig)
got = self.client.get('/web/user.com/feed')
self.assert_equals(200, got.status_code)
2023-07-28 22:49:29 +00:00
self.assert_equals(['biff'] + self.EXPECTED,
contents(microformats2.html_to_activities(got.text)))
2023-07-28 22:49:29 +00:00
# NOTE's and MENTION's authors; check for two instances
bob = '<a class="p-name u-url" href="https://plus.google.com/bob">Bob</a>'
assert got.text.index(bob) != got.text.rindex(bob)
def test_feed_atom_empty(self):
got = self.client.get('/web/user.com/feed?format=atom')
self.assert_equals(200, got.status_code)
self.assert_equals(atom.CONTENT_TYPE, got.headers['Content-Type'])
self.assert_equals([], atom.atom_to_activities(got.text))
def test_feed_atom_empty_g_user_without_obj(self):
self.user.obj_key = None
self.user.put()
self.test_feed_atom_empty()
def test_feed_atom(self):
self.add_objects()
got = self.client.get('/web/user.com/feed?format=atom')
self.assert_equals(200, got.status_code)
self.assert_equals(atom.CONTENT_TYPE, got.headers['Content-Type'])
self.assert_equals(self.EXPECTED, contents(atom.atom_to_activities(got.text)))
2023-07-28 22:49:29 +00:00
# NOTE's and MENTION's authors; check for two instances
bob = """
<uri>https://plus.google.com/bob</uri>
<name>Bob</name>
"""
assert got.text.index(bob) != got.text.rindex(bob)
# COMMENT's author
self.assertIn('Dr. Eve', got.text)
def test_feed_rss_empty(self):
got = self.client.get('/web/user.com/feed?format=rss')
self.assert_equals(200, got.status_code)
self.assert_equals(rss.CONTENT_TYPE, got.headers['Content-Type'])
self.assert_equals([], rss.to_activities(got.text))
def test_feed_rss(self):
self.add_objects()
got = self.client.get('/web/user.com/feed?format=rss')
self.assert_equals(200, got.status_code)
self.assert_equals(rss.CONTENT_TYPE, got.headers['Content-Type'])
self.assert_equals(self.EXPECTED, contents(rss.to_activities(got.text)))
2023-07-28 22:49:29 +00:00
# NOTE's and MENTION's authors; check for two instances
bob = '<author>_@_._ (Bob)</author>'
self.assertIn(bob, got.text)
self.assertNotEqual(got.text.index(bob), got.text.rindex(bob), got.text)
2023-07-28 22:49:29 +00:00
# COMMENT's author
self.assertIn('<author>_@_._ (Dr. Eve)</author>', got.text, got.text)
2023-07-28 22:49:29 +00:00
def test_nodeinfo(self):
# just check that it doesn't crash
self.client.get('/nodeinfo.json')
def test_instance_info(self):
# just check that it doesn't crash
self.client.get('/api/v1/instance')
def test_canonicalize_domain(self):
got = self.client.get('/', base_url='https://ap.brid.gy/')
self.assert_equals(301, got.status_code)
self.assert_equals('https://fed.brid.gy/', got.headers['Location'])
def test_find_user_page_web_domain(self):
got = self.client.post('/user-page', data={'id': 'user.com'})
self.assert_equals(302, got.status_code)
self.assert_equals('/web/user.com', got.headers['Location'])
def test_find_user_page_fake_id(self):
self.make_user('fake:foo', cls=Fake)
got = self.client.post('/user-page', data={'id': 'fake:foo'})
self.assert_equals(302, got.status_code)
self.assert_equals('/fa/fake:handle:foo', got.headers['Location'])
def test_find_user_page_fake_handle(self):
self.make_user('fake:foo', cls=Fake)
got = self.client.post('/user-page', data={'id': 'fake:handle:foo'})
self.assert_equals(302, got.status_code)
self.assert_equals('/fa/fake:handle:foo', got.headers['Location'])
def test_find_user_page_unknown_protocol(self):
self.make_user('fake:foo', cls=Fake)
got = self.client.post('/user-page', data={'id': 'un:kn:own'})
self.assert_equals(404, got.status_code)
self.assertEqual(["Couldn't determine network for un:kn:own."],
get_flashed_messages())
def test_find_user_page_fake_not_found(self):
got = self.client.post('/user-page', data={'id': 'fake:foo'})
self.assert_equals(404, got.status_code)
self.assertEqual(["User fake:foo on fake-phrase isn't signed up."],
get_flashed_messages())
def test_find_user_page_other_not_enabled(self):
self.make_user('other:foo', cls=OtherFake)
got = self.client.post('/user-page', data={'id': 'other:foo'})
self.assert_equals(404, got.status_code)
self.assertEqual(["User other:foo on other-phrase isn't signed up."],
get_flashed_messages())
def test_logout(self):
with self.client.session_transaction() as sess:
sess[LOGINS_SESSION_KEY] = [('BlueskyAuth', 'did:abc')]
resp = self.client.post('/logout')
self.assertEqual(302, resp.status_code)
self.assertEqual('/', resp.headers['Location'])
self.assertNotIn(LOGINS_SESSION_KEY, session)
def test_settings_no_logins(self):
resp = self.client.get('/settings')
self.assertEqual(302, resp.status_code)
self.assertEqual('/login', resp.headers['Location'])
def test_settings(self):
self.make_user('http://b.c/a', cls=ActivityPub, enabled_protocols=['fake'],
obj_as2={
'id': 'http://b.c/a',
'preferredUsername': 'a',
'icon': 'http://b/c/a.jpg',
})
self.store_object(id='did:plc:abc', raw={'alsoKnownAs': ['at://ab.c']})
self.make_user('did:plc:abc', cls=ATProto)
BlueskyAuth(id='did:plc:abc', user_json='{}').put()
MastodonAuth(id='@a@b.c', access_token_str='',
user_json='{"uri":"http://b.c/a"}').put()
with self.client.session_transaction() as sess:
sess[LOGINS_SESSION_KEY] = [
('BlueskyAuth', 'did:plc:abc'),
('MastodonAuth', '@a@b.c'),
]
resp = self.client.get('/settings')
self.assertEqual(200, resp.status_code)
body = resp.get_data(as_text=True)
self.assert_multiline_in('<a href="/ap/@a@b.c">Currently bridging.</a>', body)
self.assert_multiline_in('<a class="h-card u-author" rel="me" href="https://bsky.app/profile/ab.c" title="ab.c">ab.c</a>', body)
self.assert_multiline_in('Not bridging.', body)
@patch('requests.get')
def test_settings_on_login_create_new_user(self, mock_get):
mock_get.return_value = self.as2_resp(ACTOR_AS2)
auth = MastodonAuth(id='@a@b.c', access_token_str='',
user_json='{"uri":"http://b.c/a"}').put()
with self.client.session_transaction() as sess:
sess[LOGINS_SESSION_KEY] = [('MastodonAuth', '@a@b.c')]
resp = self.client.get(f'/settings?auth_entity={auth.urlsafe().decode()}')
self.assertEqual(200, resp.status_code)
user = ActivityPub.get_by_id('http://b.c/a', allow_opt_out=True)
self.assertEqual(ACTOR_AS2, user.obj.as2)
body = resp.get_data(as_text=True)
self.assert_multiline_in('Not bridging because you haven&#39;t set a profile picture.', body)
@patch('pages.PROTOCOLS', new={
'activitypub': ActivityPub,
'efake': ExplicitFake,
'web': Web,
})
def test_enable(self):
bot = self.make_user(id='efake.brid.gy', cls=Web)
user = self.make_user('http://b.c/a', cls=ActivityPub, obj_as2={
'id': 'http://b.c/a',
'preferredUsername': 'a',
'icon': 'http://b/c/a.jpg',
})
auth = MastodonAuth(id='@a@b.c', access_token_str='',
user_json='{"uri":"http://b.c/a"}').put()
with self.client.session_transaction() as sess:
sess[LOGINS_SESSION_KEY] = [('MastodonAuth', '@a@b.c')]
resp = self.client.post(f'/settings/enable', data={
'key': user.key.urlsafe().decode(),
})
self.assertEqual(302, resp.status_code)
self.assertEqual('/settings', resp.headers['Location'])
self.assertEqual(['Now bridging @a@b.c to efake-phrase.'],
get_flashed_messages())
self.assertEqual(['efake'], user.key.get().enabled_protocols)
self.assertEqual(['http://b.c/a'], ExplicitFake.created_for)
def test_enable_not_logged_in(self):
self.store_object(id='did:plc:abc', raw={})
user = self.make_user('did:plc:abc', cls=ATProto, enabled_protocols=[])
BlueskyAuth(id='did:plc:abc', user_json='{}').put()
with self.client.session_transaction() as sess:
sess[LOGINS_SESSION_KEY] = [('BlueskyAuth', 'did:plc:abc')]
resp = self.client.post(f'/settings/enable', data={
'key': ExplicitFake(id='efake:user').key.urlsafe().decode(),
})
self.assertEqual(302, resp.status_code)
self.assertEqual('/login', resp.headers['Location'])
self.assertEqual([], user.key.get().enabled_protocols)
self.assertEqual([], ExplicitFake.created_for)
def test_disable(self):
self.store_object(id='did:plc:abc', raw={})
user = self.make_user('did:plc:abc', cls=ATProto, enabled_protocols=['efake'])
BlueskyAuth(id='did:plc:abc', user_json='{}').put()
with self.client.session_transaction() as sess:
sess[LOGINS_SESSION_KEY] = [('BlueskyAuth', 'did:plc:abc')]
resp = self.client.post(f'/settings/disable', data={
'key': user.key.urlsafe().decode(),
})
self.assertEqual(302, resp.status_code)
self.assertEqual('/settings', resp.headers['Location'])
self.assertEqual(['Disabled bridging did:plc:abc to efake-phrase.'],
get_flashed_messages())
self.assertEqual([], user.key.get().enabled_protocols)
def test_disable_not_logged_in(self):
user = self.make_user('http://b.c/a', cls=ActivityPub,
enabled_protocols=['efake'], obj_as2={
'id': 'http://b.c/a',
})
MastodonAuth(id='@a@b.c', access_token_str='',
user_json='{"uri":"http://b.c/a"}').put()
with self.client.session_transaction() as sess:
sess[LOGINS_SESSION_KEY] = [('MastodonAuth', '@a@b.c')]
resp = self.client.post(f'/settings/disable', data={
'key': ExplicitFake(id='efake:user').key.urlsafe().decode(),
})
self.assertEqual(302, resp.status_code)
self.assertEqual('/login', resp.headers['Location'])
self.assertEqual(['efake'], user.key.get().enabled_protocols)