"""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 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 import config from models import Object, Follower, Target from web import Web from granary.tests.test_bluesky import 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', } 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( '', 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): 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: 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('/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']) self.assertEqual( ['Updating profile from fake:handle:user...'], 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, deleted=False) 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( ['Couldn\'t update profile for fake:handle:user: 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', } 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) self.assert_equals(['biff'] + self.EXPECTED, contents(microformats2.html_to_activities(got.text))) # NOTE's and MENTION's authors; check for two instances bob = 'Bob' 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))) # NOTE's and MENTION's authors; check for two instances bob = """ https://plus.google.com/bob Bob """ 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))) # NOTE's and MENTION's authors; check for two instances bob = '_@_._ (Bob)' self.assertIn(bob, got.text) self.assertNotEqual(got.text.index(bob), got.text.rindex(bob), got.text) # COMMENT's author self.assertIn('_@_._ (Dr. Eve)', got.text, got.text) 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_find_user_page_xss_unknown_protocol(self): """Test that HTML in id parameter is escaped for unknown protocol.""" got = self.client.post('/user-page', data={'id': ''}) self.assert_equals(404, got.status_code) self.assertEqual(["Couldn't determine network for <script>alert("xss")</script>."], get_flashed_messages()) def test_find_user_page_xss_user_not_found(self): """Test that HTML in id parameter is escaped for user not found.""" got = self.client.post('/user-page', data={'id': 'fake:'}) self.assert_equals(404, got.status_code) self.assertEqual(["User fake:<script>alert("xss")</script> on fake-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('Bridging: ', body) self.assert_multiline_in('ab.c', body) # TODO: bring back # self.assert_multiline_in('Not bridging.', body) def test_settings_private_status(self): # the enable switch should be enabled even if the user is status=private self.make_user('http://b.c/a', cls=ActivityPub, obj_as1={ 'image': 'http://pic', 'to': [{'alias': '@private'}], }) 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('/settings') self.assertEqual(200, resp.status_code) body = resp.get_data(as_text=True) self.assert_multiline_in( 'Not bridging because your account is set as private', body) self.assert_multiline_in( '', 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 your account doesn't have a profile picture', body) @patch('requests.get', side_effect=[ requests_response(DID_DOC), requests_response({ 'uri': 'at://did:plc:abc/app.bsky.actor.profile/self', 'cid': 'bafyreigd', 'value': ACTOR_PROFILE_BSKY, }), ]) def test_settings_on_login_reload_profile(self, mock_get): self.store_object(id='did:plc:abc', raw={'alsoKnownAs': ['at://ab.c']}) user = self.make_user('did:plc:abc', cls=ATProto) auth = 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.get(f'/settings?auth_entity={auth.urlsafe().decode()}') self.assertEqual(200, resp.status_code) user = user.key.get() self.assertEqual({ **ACTOR_PROFILE_BSKY, 'cid': 'bafyreigd', }, user.obj.bsky) @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('/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('/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('/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('/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) def test_set_username(self): user = self.make_user('http://b.c/a', cls=ActivityPub, enabled_protocols=['other'], 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('/settings/set-username', data={ 'key': user.key.urlsafe().decode(), 'protocol': 'other', 'username': 'yoozer', }) self.assertEqual(302, resp.status_code) self.assertEqual('/settings', resp.headers['Location']) self.assertEqual(['Setting username on other-phrase to yoozer...'], get_flashed_messages()) self.assertEqual('yoozer', OtherFake.usernames['http://b.c/a']) def test_toggle_notifs(self): user = self.make_user('http://b.c/a', cls=ActivityPub, send_notifs='none', obj_as2={'id': 'http://b.c/a', 'preferredUsername': 'a'}) 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')] # toggle on resp = self.client.post('/settings/toggle-notifs', data={ 'key': user.key.urlsafe().decode(), }) self.assertEqual(302, resp.status_code) self.assertEqual('/settings', resp.headers['Location']) self.assertEqual(['DM notifications enabled for @a@b.c.'], get_flashed_messages()) self.assertEqual('all', user.key.get().send_notifs) # toggle off resp = self.client.post('/settings/toggle-notifs', data={ 'key': user.key.urlsafe().decode(), }) self.assertEqual(302, resp.status_code) self.assertEqual('/settings', resp.headers['Location']) self.assertEqual(['DM notifications enabled for @a@b.c.', 'DM notifications disabled for @a@b.c.'], get_flashed_messages()) self.assertEqual('none', user.key.get().send_notifs) def test_memcache_evict(self): # key = Fake(id='fake:foo').put() self.user.key.get() self.assertIsNotNone(self.user.key.get(use_cache=False, use_datastore=False, use_global_cache=True)) resp = self.client.post('/admin/memcache-evict', data={'key': self.user.key.urlsafe().decode()}, headers={'Authorization': config.SECRET_KEY}) self.assertEqual(200, resp.status_code) self.assertIsNone(self.user.key.get(use_cache=False, use_datastore=False, use_global_cache=True)) def test_memcache_evict_bad_auth(self): self.user.key.get() self.assertIsNotNone(self.user.key.get(use_cache=False, use_datastore=False, use_global_cache=True)) resp = self.client.post('/admin/memcache-evict', data={ 'key': self.user.key.urlsafe().decode(), }) self.assertEqual(401, resp.status_code) self.assertIsNotNone(self.user.key.get(use_cache=False, use_datastore=False, use_global_cache=True))