kopia lustrzana https://github.com/snarfed/bridgy-fed
3855 wiersze
138 KiB
Python
3855 wiersze
138 KiB
Python
"""Unit tests for protocol.py."""
|
|
import copy
|
|
from datetime import timedelta
|
|
import logging
|
|
from threading import Condition, Thread
|
|
from unittest import skip
|
|
from unittest.mock import ANY, patch
|
|
|
|
from arroba.tests.testutil import dns_answer
|
|
from google.cloud import ndb
|
|
from google.cloud.ndb.global_cache import _InProcessGlobalCache
|
|
from granary import as2
|
|
from granary.tests.test_bluesky import ACTOR_PROFILE_BSKY
|
|
from oauth_dropins.webutil import appengine_info, util
|
|
from oauth_dropins.webutil.appengine_config import ndb_client
|
|
from oauth_dropins.webutil.flask_util import NoContent
|
|
from oauth_dropins.webutil.testutil import NOW, requests_response
|
|
from oauth_dropins.webutil.util import json_dumps
|
|
import requests
|
|
from werkzeug.exceptions import BadRequest
|
|
|
|
# import first so that Fake is defined before URL routes are registered
|
|
from .testutil import ExplicitFake, Fake, OtherFake, TestCase
|
|
|
|
from activitypub import ActivityPub
|
|
from app import app
|
|
from atproto import ATProto
|
|
import common
|
|
from common import ErrorButDoNotRetryTask
|
|
import memcache
|
|
import models
|
|
from models import DM, Follower, Object, PROTOCOLS, Target, User
|
|
import protocol
|
|
from protocol import Protocol
|
|
from ui import UIProtocol
|
|
from web import Web
|
|
|
|
from .test_activitypub import ACTOR, NOTE
|
|
from .test_atproto import DID_DOC
|
|
from . import test_dms
|
|
from .test_web import (
|
|
ACTOR_HTML_RESP,
|
|
ACTOR_AS1_UNWRAPPED_URLS,
|
|
ACTOR_MF2_REL_URLS,
|
|
NOTE as NOTE_HTML_RESP,
|
|
web_user_gets,
|
|
)
|
|
|
|
|
|
class ProtocolTest(TestCase):
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.user = self.make_user('foo.com', cls=Web, has_hcard=True)
|
|
|
|
def tearDown(self):
|
|
PROTOCOLS.pop('greedy', None)
|
|
super().tearDown()
|
|
|
|
def test_protocols_global(self):
|
|
self.assertEqual(Fake, PROTOCOLS['fake'])
|
|
self.assertEqual(Web, PROTOCOLS['web'])
|
|
self.assertEqual(Web, PROTOCOLS['webmention'])
|
|
|
|
def test_for_bridgy_subdomain_for_request(self):
|
|
for domain, expected in [
|
|
('fake.brid.gy', Fake),
|
|
('ap.brid.gy', ActivityPub),
|
|
('activitypub.brid.gy', ActivityPub),
|
|
('web.brid.gy', Web),
|
|
(None, None),
|
|
('', None),
|
|
('brid.gy', None),
|
|
('www.brid.gy', None),
|
|
('fed.brid.gy', None),
|
|
('fake.fed.brid.gy', None),
|
|
('fake', None),
|
|
('fake.com', None),
|
|
]:
|
|
with self.subTest(domain=domain, expected=expected):
|
|
self.assertEqual(expected, Protocol.for_bridgy_subdomain(domain))
|
|
with app.test_request_context('/foo', base_url=f'https://{domain}/'):
|
|
self.assertEqual(expected, Protocol.for_request())
|
|
|
|
def test_for_bridgy_subdomain_for_request_fed(self):
|
|
for url, expected in [
|
|
('https://fed.brid.gy/', Fake),
|
|
('http://localhost/foo', Fake),
|
|
('https://ap.brid.gy/bar', ActivityPub),
|
|
('https://baz/biff', None),
|
|
]:
|
|
with self.subTest(url=url, expected=expected):
|
|
self.assertEqual(expected,
|
|
Protocol.for_bridgy_subdomain(url, fed=Fake))
|
|
self.assertEqual(expected,
|
|
Protocol.for_bridgy_subdomain(url, fed='fake'))
|
|
with app.test_request_context('/foo', base_url=url):
|
|
self.assertEqual(expected, Protocol.for_request(fed=Fake))
|
|
|
|
def test_for_id(self):
|
|
for id, expected in [
|
|
(None, None),
|
|
('', None),
|
|
('foo://bar', None),
|
|
('https:/abc/', None),
|
|
('https:/[DOMAIN]/', None),
|
|
('https://[DOMAIN]/', None),
|
|
('#bridgy-fed-foo', None),
|
|
('fake:foo', Fake),
|
|
('at://foo', ATProto),
|
|
# TODO: remove? should we require normalized ids?
|
|
('https://ap.brid.gy/foo/bar', ActivityPub),
|
|
('https://web.brid.gy/foo/bar', Web),
|
|
('https://ap.brid.gy/internal/foo', Web),
|
|
('https://bsky.brid.gy/internal/foo', Web),
|
|
('https://fed.brid.gy/', Web),
|
|
('https://web.brid.gy/', Web),
|
|
('https://bsky.brid.gy/', Web),
|
|
('bsky.brid.gy', Web),
|
|
]:
|
|
with self.subTest(id=id, expected=expected):
|
|
self.assertEqual(expected, Protocol.for_id(id, remote=False))
|
|
self.assertEqual(expected, Protocol.for_id(id, remote=True))
|
|
|
|
def test_for_id_true_overrides_none(self):
|
|
class Greedy(Protocol, User):
|
|
@classmethod
|
|
def owns_id(cls, id):
|
|
return True
|
|
|
|
self.assertEqual(Greedy, Protocol.for_id('http://foo'))
|
|
self.assertEqual(Greedy, Protocol.for_id('https://bar/baz'))
|
|
|
|
def test_for_id_object(self):
|
|
self.store_object(id='http://ui.org/obj', source_protocol='ui')
|
|
self.assertEqual(UIProtocol, Protocol.for_id('http://ui.org/obj'))
|
|
|
|
@patch('requests.get', return_value=requests_response())
|
|
def test_for_id_object_missing_source_protocol(self, _):
|
|
self.store_object(id='http://b.ad/obj')
|
|
self.assertIsNone(Protocol.for_id('http://b.ad/obj'))
|
|
|
|
@patch('requests.get')
|
|
def test_for_id_activitypub_fetch(self, mock_get):
|
|
mock_get.return_value = self.as2_resp(ACTOR)
|
|
self.assertEqual(ActivityPub, Protocol.for_id('http://ap.org/actor'))
|
|
self.assertIn(self.as2_req('http://ap.org/actor'), mock_get.mock_calls)
|
|
|
|
@patch('requests.get')
|
|
def test_for_id_activitypub_fetch_fails(self, mock_get):
|
|
mock_get.return_value = requests_response('', status=403)
|
|
self.assertIsNone(Protocol.for_id('http://ap.org/actor'))
|
|
self.assertIn(self.as2_req('http://ap.org/actor'), mock_get.mock_calls)
|
|
mock_get.assert_called_once()
|
|
|
|
@patch('requests.get')
|
|
def test_for_id_web_fetch(self, mock_get):
|
|
mock_get.return_value = ACTOR_HTML_RESP
|
|
self.assertEqual(Web, Protocol.for_id('http://web.site/'))
|
|
self.assertIn(self.req('http://web.site/'), mock_get.mock_calls)
|
|
|
|
@patch('requests.get')
|
|
def test_for_id_web_fetch_not_html(self, mock_get):
|
|
mock_get.return_value = requests_response('not html', content_type='text/abc')
|
|
self.assertIsNone(Protocol.for_id('http://web.site/xyz'))
|
|
self.assertIsNone(Object.get_by_id('http://web.site/xyz'))
|
|
self.assertIn(self.req('http://web.site/xyz'), mock_get.mock_calls)
|
|
|
|
@patch('requests.get')
|
|
def test_for_id_web_fetch_no_mf2(self, mock_get):
|
|
mock_get.return_value = requests_response('<html></html>')
|
|
self.assertEqual(Web, Protocol.for_id('http://web.site/xyz'))
|
|
|
|
obj = self.assert_object('http://web.site/xyz', source_protocol='web')
|
|
self.assertIsNone(obj.mf2)
|
|
self.assertIsNone(obj.as1)
|
|
|
|
self.assertIn(self.req('http://web.site/xyz'), mock_get.mock_calls)
|
|
|
|
@patch('requests.get')
|
|
def test_for_id_web_remote_false(self, mock_get):
|
|
self.assertIsNone(Protocol.for_id('http://web.site/', remote=False))
|
|
mock_get.assert_not_called()
|
|
|
|
@patch('requests.get')
|
|
def test_for_id_synthetic(self, mock_get):
|
|
self.assertEqual(ATProto, Protocol.for_id('at://did/coll/rkey#bridgy-fed-xyz'))
|
|
self.assertEqual(Fake, Protocol.for_id('fake:post#bridgy-fed-delete-abc'))
|
|
|
|
Object(id='http://in.st/post', source_protocol='activitypub').put()
|
|
self.assertEqual(ActivityPub,
|
|
Protocol.for_id('http://in.st/post#bridgy-fed-a'))
|
|
|
|
mock_get.assert_not_called()
|
|
|
|
def test_for_handle_deterministic(self):
|
|
for handle, expected in [
|
|
(None, (None, None)),
|
|
('', (None, None)),
|
|
('foo://bar', (None, None)),
|
|
('fake:foo', (None, None)),
|
|
('fake:handle:foo', (Fake, None)),
|
|
('@me@foo', (ActivityPub, None)),
|
|
]:
|
|
self.assertEqual(expected, Protocol.for_handle(handle))
|
|
|
|
def test_for_handle_stored_user(self):
|
|
user = self.make_user(id='user.com', cls=Web)
|
|
self.assertEqual('user.com', user.handle)
|
|
self.assertEqual((Web, 'user.com'), Protocol.for_handle('user.com'))
|
|
|
|
def test_for_handle_opted_out_user(self):
|
|
user = self.make_user(id='user.com', cls=Web, obj_as1={'summary': '#nobot'})
|
|
self.assertEqual('user.com', user.handle)
|
|
self.assertEqual((None, None), Protocol.for_handle('user.com'))
|
|
|
|
@patch('dns.resolver.resolve', return_value = dns_answer(
|
|
'_atproto.ha.nl.', '"did=did:plc:123abc"'))
|
|
def test_for_handle_atproto_resolve(self, _):
|
|
self.assertEqual((ATProto, 'did:plc:123abc'), Protocol.for_handle('ha.nl'))
|
|
|
|
def test_is_user_at_domain(self):
|
|
for handle in ('user@instance', 'user@instance.com', 'user.com@instance.com',
|
|
'_@sub.do.main'):
|
|
with self.subTest(handle=handle):
|
|
self.assertTrue(ActivityPub.is_user_at_domain(handle))
|
|
|
|
for handle in ('instance', 'instance.com', '@user', 'x@y@user.com',
|
|
'http://user.com', '@user@web.brid.gy', '@user@localhost'):
|
|
with self.subTest(handle=handle):
|
|
self.assertEqual(False, ActivityPub.is_user_at_domain(handle))
|
|
|
|
assert ActivityPub.is_user_at_domain('user@web.brid.gy', allow_internal=True)
|
|
|
|
def test_load(self):
|
|
Fake.fetchable['foo'] = {'x': 'y'}
|
|
|
|
loaded = Fake.load('foo')
|
|
self.assertEqual({
|
|
'id': 'foo',
|
|
'x': 'y',
|
|
}, loaded.our_as1)
|
|
self.assertFalse(loaded.changed)
|
|
self.assertTrue(loaded.new)
|
|
|
|
self.assertIsNotNone(Object.get_by_id('foo'))
|
|
self.assertEqual(['foo'], Fake.fetched)
|
|
|
|
def test_load_existing(self):
|
|
self.store_object(id='foo', our_as1={'x': 'y'})
|
|
|
|
loaded = Fake.load('foo')
|
|
self.assertEqual({
|
|
'id': 'foo',
|
|
'x': 'y',
|
|
}, loaded.our_as1)
|
|
self.assertFalse(loaded.changed)
|
|
self.assertFalse(loaded.new)
|
|
|
|
self.assertEqual([], Fake.fetched)
|
|
|
|
def test_load_existing_empty_deleted(self):
|
|
stored = self.store_object(id='foo', deleted=True)
|
|
|
|
loaded = Fake.load('foo')
|
|
self.assert_entities_equal(stored, loaded)
|
|
self.assertFalse(loaded.changed)
|
|
self.assertFalse(loaded.new)
|
|
|
|
self.assertEqual([], Fake.fetched)
|
|
|
|
def test_load_remote_true_existing_empty(self):
|
|
Fake.fetchable['foo'] = {'x': 'y'}
|
|
Object(id='foo', our_as1={}).put()
|
|
|
|
loaded = Fake.load('foo', remote=True)
|
|
self.assertEqual({'id': 'foo', 'x': 'y'}, loaded.as1)
|
|
self.assertTrue(loaded.changed)
|
|
self.assertFalse(loaded.new)
|
|
self.assertEqual(['foo'], Fake.fetched)
|
|
|
|
def test_load_remote_true_new_empty(self):
|
|
Fake.fetchable['foo'] = None
|
|
self.store_object(id='foo', our_as1={'x': 'y'})
|
|
|
|
loaded = Fake.load('foo', remote=True)
|
|
self.assertIsNone(loaded.as1)
|
|
self.assertTrue(loaded.changed)
|
|
self.assertFalse(loaded.new)
|
|
self.assertEqual(['foo'], Fake.fetched)
|
|
|
|
def test_load_remote_true_unchanged(self):
|
|
obj = self.store_object(id='fake:foo', our_as1={'x': 'stored'},
|
|
source_protocol='fake')
|
|
Fake.fetchable['fake:foo'] = {'x': 'stored'}
|
|
|
|
loaded = Fake.load('fake:foo', remote=True)
|
|
self.assert_entities_equal(obj, loaded,
|
|
ignore=['expire', 'created', 'updated'])
|
|
self.assertFalse(loaded.changed)
|
|
self.assertFalse(loaded.new)
|
|
self.assertEqual(['fake:foo'], Fake.fetched)
|
|
|
|
def test_load_remote_true_local_false(self):
|
|
Fake.fetchable['foo'] = our_as1={'x': 'y'}
|
|
|
|
loaded = Fake.load('foo', local=False, remote=True)
|
|
self.assertEqual({'id': 'foo', 'x': 'y'}, loaded.as1)
|
|
self.assertIsNone(loaded.changed)
|
|
self.assertIsNone(loaded.new)
|
|
self.assertEqual(['foo'], Fake.fetched)
|
|
|
|
def test_load_remote_true_changed(self):
|
|
self.store_object(id='foo', our_as1={'content': 'stored'})
|
|
Fake.fetchable['foo'] = {'content': 'new'}
|
|
|
|
loaded = Fake.load('foo', remote=True)
|
|
self.assertEqual({
|
|
'id': 'foo',
|
|
'content': 'new',
|
|
}, loaded.our_as1)
|
|
self.assertTrue(loaded.changed)
|
|
self.assertFalse(loaded.new)
|
|
self.assertEqual(['foo'], Fake.fetched)
|
|
|
|
@patch('requests.get', return_value=ACTOR_HTML_RESP)
|
|
def test_load_remote_true_clear_our_as1(self, _):
|
|
self.store_object(id='https://f.ooo', our_as1={'should': 'disappear'},
|
|
source_protocol='web')
|
|
|
|
expected_mf2 = {
|
|
'type': ['h-card'],
|
|
'properties': {
|
|
'url': ['https://f.ooo', 'https://user.com/'],
|
|
'name': ['Ms. ☕ Baz'],
|
|
},
|
|
'rel-urls': {
|
|
'https://f.ooo': {'rels': ['me'], 'text': 'Ms. ☕ Baz'},
|
|
'https://user.com/webmention': {'rels': ['webmention'], 'text': ''},
|
|
},
|
|
'url': 'https://f.ooo',
|
|
}
|
|
|
|
loaded = Web.load('https://f.ooo', remote=True)
|
|
self.assertEqual(expected_mf2, loaded.mf2)
|
|
self.assertIsNone(loaded.our_as1)
|
|
self.assertEqual({
|
|
'objectType': 'person',
|
|
'id': 'https://f.ooo',
|
|
'url': 'https://f.ooo',
|
|
'displayName': 'Ms. ☕ Baz',
|
|
'urls': [
|
|
{'value': 'https://f.ooo', 'displayName': 'Ms. ☕ Baz'},
|
|
{'value': 'https://user.com/'},
|
|
],
|
|
} , loaded.as1)
|
|
self.assertTrue(loaded.changed)
|
|
self.assertFalse(loaded.new)
|
|
|
|
def test_load_remote_false(self):
|
|
self.assertIsNone(Fake.load('nope', remote=False))
|
|
self.assertEqual([], Fake.fetched)
|
|
|
|
obj = self.store_object(id='foo', our_as1={'content': 'stored'})
|
|
self.assert_entities_equal(obj, Fake.load('foo', remote=False))
|
|
self.assertEqual([], Fake.fetched)
|
|
|
|
def test_load_remote_false_existing_object_empty(self):
|
|
obj = self.store_object(id='foo')
|
|
self.assert_entities_equal(obj, Protocol.load('foo', remote=False))
|
|
|
|
def test_load_local_false_missing(self):
|
|
self.assertIsNone(Fake.load('foo', local=False))
|
|
self.assertEqual(['foo'], Fake.fetched)
|
|
|
|
def test_load_local_false_existing(self):
|
|
self.store_object(id='foo', our_as1={'content': 'stored'}, source_protocol='ui')
|
|
|
|
Fake.fetchable['foo'] = {'foo': 'bar'}
|
|
Fake.load('foo', local=False)
|
|
self.assert_object('foo', source_protocol='fake', our_as1={'foo': 'bar'})
|
|
self.assertEqual(['foo'], Fake.fetched)
|
|
|
|
def test_load_remote_false_local_false_assert(self):
|
|
with self.assertRaises(AssertionError):
|
|
Fake.load('nope', local=False, remote=False)
|
|
|
|
def test_load_resolve_ids(self):
|
|
follow = {
|
|
'objectType': 'activity',
|
|
'verb': 'follow',
|
|
'id': 'fake:follow',
|
|
'actor': 'fake:alice',
|
|
'object': 'fake:bob',
|
|
}
|
|
Fake.fetchable = {
|
|
'fake:follow': follow,
|
|
}
|
|
|
|
# no matching copy users
|
|
obj = Fake.load('fake:follow', remote=True)
|
|
self.assert_equals(follow, obj.our_as1)
|
|
|
|
# matching copy user
|
|
self.make_user('other:bob', cls=OtherFake,
|
|
copies=[Target(uri='fake:bob', protocol='fake')])
|
|
|
|
obj = Fake.load('fake:follow', remote=True)
|
|
self.assert_equals({
|
|
**follow,
|
|
'id': 'fake:follow',
|
|
'object': 'other:bob',
|
|
}, obj.our_as1)
|
|
|
|
def test_load_preserves_fragment(self):
|
|
stored = self.store_object(id='http://the/id#frag', our_as1={'foo': 'bar'})
|
|
got = ActivityPub.load('http://the/id#frag')
|
|
self.assert_entities_equal(stored, got)
|
|
self.assertEqual([], Fake.fetched)
|
|
|
|
def test_load_refresh(self):
|
|
Fake.fetchable['foo'] = {'fetched': 'x'}
|
|
|
|
too_old = (NOW.replace(tzinfo=None)
|
|
- protocol.OBJECT_REFRESH_AGE
|
|
- timedelta(days=1))
|
|
with patch('models.Object.updated._now', return_value=too_old):
|
|
obj = Object(id='foo', our_as1={'orig': 'y'})
|
|
obj.put()
|
|
|
|
loaded = Fake.load('foo')
|
|
self.assertEqual({'fetched': 'x', 'id': 'foo'}, loaded.our_as1)
|
|
|
|
@patch('oauth_dropins.webutil.models.MAX_ENTITY_SIZE', new=50)
|
|
def test_load_too_big(self):
|
|
Fake.fetchable['fake:foo'] = {
|
|
'objectType': 'note',
|
|
'content': 'a bit of text that makes sure we end up over the limit ',
|
|
}
|
|
self.assertIsNone(Fake.load('fake:foo'))
|
|
|
|
def test_actor_key(self):
|
|
user = self.make_user(id='fake:a', cls=Fake)
|
|
a_key = user.key
|
|
|
|
for expected, obj in [
|
|
(None, Object()),
|
|
(None, Object(our_as1={})),
|
|
(None, Object(our_as1={'foo': 'bar'})),
|
|
(None, Object(our_as1={'foo': 'bar'})),
|
|
(None, Object(our_as1={'actor': ''})),
|
|
(a_key, Object(our_as1={'actor': 'fake:a'})),
|
|
(a_key, Object(our_as1={'author': 'fake:a'})),
|
|
]:
|
|
self.assertEqual(expected, Fake.actor_key(obj))
|
|
|
|
self.assertIsNone(Fake.actor_key(Object()))
|
|
|
|
def test_key_for(self):
|
|
self.assertEqual(self.user.key, Protocol.key_for(self.user.key.id()))
|
|
|
|
user = Fake(id='fake:other', use_instead=self.user.key).put()
|
|
self.assertEqual(self.user.key, Protocol.key_for('fake:other'))
|
|
|
|
# no stored user
|
|
self.assertEqual(ndb.Key('Fake', 'fake:foo'), Protocol.key_for('fake:foo'))
|
|
|
|
self.user.obj.our_as1 = {'summary': '#nobridge'}
|
|
self.user.obj.put()
|
|
self.assertIsNone(Protocol.key_for(self.user.key.id()))
|
|
|
|
def test_bridged_web_url_for(self):
|
|
self.assertIsNone(Protocol.bridged_web_url_for(self.user))
|
|
self.assertEqual('https://foo.com/',
|
|
Protocol.bridged_web_url_for(self.user, fallback=True))
|
|
|
|
def test_targets_checks_blocklisted_per_protocol(self):
|
|
"""_targets should call the target protocol's is_blocklisted()."""
|
|
# non-ATProto account, ATProto target (PDS) is bsky.brid.gy
|
|
# shouldn't be blocklisted
|
|
user = self.make_user(id='fake:user', cls=Fake, enabled_protocols=['atproto'])
|
|
|
|
did_doc = copy.deepcopy(DID_DOC)
|
|
did_doc['service'][0]['serviceEndpoint'] = 'http://localhost'
|
|
self.store_object(id='did:plc:foo', raw=did_doc)
|
|
|
|
# store Objects so we don't try to fetch them remotely
|
|
self.store_object(id='at://did:plc:foo/co.ll/post', our_as1={'foo': 'bar'})
|
|
self.store_object(id='fake:post', our_as1={'foo': 'baz'})
|
|
|
|
obj = Object(source_protocol='other', our_as1={
|
|
'id': 'other:reply',
|
|
'objectType': 'note',
|
|
'inReplyTo': [
|
|
'fake:post',
|
|
'fake:blocklisted-post',
|
|
'https://t.co/foo',
|
|
'http://localhost/post',
|
|
'at://did:plc:foo/co.ll/post',
|
|
],
|
|
})
|
|
self.assertCountEqual([
|
|
Target(protocol='fake', uri='fake:post:target'),
|
|
Target(protocol='atproto', uri='https://atproto.brid.gy'),
|
|
], Protocol.targets(obj, from_user=user).keys())
|
|
|
|
def test_targets_undo_share_enabled_protocols(self):
|
|
# https://console.cloud.google.com/errors/detail/CJK54eaoneesMg;time=P30D?project=bridgy-federated
|
|
self.user = self.make_user('fake:user', cls=Fake)
|
|
|
|
self.store_object(id='fake:orig', our_as1={'id': 'fake:orig'},
|
|
copies=[Target(protocol='other', uri='other:orig')])
|
|
|
|
share = {
|
|
'objectType': 'activity',
|
|
'verb': 'share',
|
|
'id': 'fake:share',
|
|
'actor': 'fake:user',
|
|
'object': 'fake:orig',
|
|
}
|
|
Fake.fetchable['fake:share'] = share
|
|
|
|
obj = Object(id='fake:undo', our_as1={
|
|
'objectType': 'activity',
|
|
'verb': 'undo',
|
|
'actor': 'fake:user',
|
|
'object': share,
|
|
})
|
|
self.assertEqual({
|
|
Target(protocol='other', uri='other:orig:target'),
|
|
}, Fake.targets(obj, from_user=self.user).keys())
|
|
|
|
def test_targets_composite_inreplyto(self):
|
|
Fake.fetchable['fake:post'] = {
|
|
'objectType': 'note',
|
|
}
|
|
|
|
reply = Object(our_as1={
|
|
'id': 'other:reply',
|
|
'objectType': 'note',
|
|
'inReplyTo': {
|
|
'id': 'fake:post',
|
|
'url': 'http://foo',
|
|
},
|
|
})
|
|
create = Object(our_as1={
|
|
'objectType': 'activity',
|
|
'verb': 'post',
|
|
'object': reply.as1,
|
|
})
|
|
|
|
self.assertEqual(
|
|
{Target(protocol='fake', uri='fake:post:target')},
|
|
OtherFake.targets(create, crud_obj=reply, from_user=self.user).keys())
|
|
|
|
def test_targets_link_tag_has_no_orig_obj(self):
|
|
# https://github.com/snarfed/bridgy-fed/issues/1237
|
|
Fake.fetchable['fake:linked-post'] = {
|
|
'objectType': 'note',
|
|
}
|
|
|
|
note = Object(our_as1={
|
|
'objectType': 'note',
|
|
'id': 'fake:post',
|
|
'tags': [{'url': 'fake:linked-post'}],
|
|
})
|
|
create = Object(our_as1={
|
|
'objectType': 'activity',
|
|
'verb': 'post',
|
|
'object': note.as1,
|
|
})
|
|
self.assertEqual(
|
|
{Target(protocol='fake', uri='fake:linked-post:target'): None},
|
|
OtherFake.targets(create, crud_obj=note, from_user=self.user))
|
|
|
|
@patch.object(Fake, 'fetch')
|
|
def test_targets_continues_on_fetch_error(self, mock_fetch):
|
|
def fetch(obj, **_):
|
|
if obj.key.id() == 'fake:post-1':
|
|
raise requests.ConnectionError('foo')
|
|
elif obj.key.id() == 'fake:post-2':
|
|
obj.our_as1 = {
|
|
'objectType': 'note',
|
|
'id': 'fake:post-2',
|
|
'author': 'fake:user',
|
|
}
|
|
return True
|
|
|
|
mock_fetch.side_effect = fetch
|
|
|
|
reply = Object(source_protocol='other', our_as1={
|
|
'id': 'other:reply',
|
|
'objectType': 'note',
|
|
'author': 'other:user',
|
|
'inReplyTo': [
|
|
'fake:post-1',
|
|
'fake:post-2',
|
|
],
|
|
})
|
|
create = Object(source_protocol='other', our_as1={
|
|
'objectType': 'activity',
|
|
'verb': 'post',
|
|
'object': reply.as1,
|
|
})
|
|
|
|
self.assertEqual(
|
|
{Target(protocol='fake', uri='fake:post-2:target')},
|
|
OtherFake.targets(create, crud_obj=reply, from_user=self.user).keys())
|
|
|
|
def test_translate_ids_follow(self):
|
|
self.assert_equals({
|
|
'id': 'other:o:fa:fake:follow',
|
|
'objectType': 'activity',
|
|
'verb': 'follow',
|
|
'actor': 'other:u:fake:alice',
|
|
'object': 'other:u:fake:bob',
|
|
}, OtherFake.translate_ids({
|
|
'id': 'fake:follow',
|
|
'objectType': 'activity',
|
|
'verb': 'follow',
|
|
'actor': 'fake:alice',
|
|
'object': 'fake:bob',
|
|
}))
|
|
|
|
def test_translate_ids_reply(self):
|
|
self.assert_equals({
|
|
'objectType': 'activity',
|
|
'verb': 'post',
|
|
'object': {
|
|
'id': 'other:o:fa:fake:reply',
|
|
'objectType': 'note',
|
|
'inReplyTo': 'other:o:fa:fake:post',
|
|
'author': 'other:u:fake:alice',
|
|
'tags': [{
|
|
'objectType': 'mention',
|
|
'url': 'uri:other:u:fake:bob',
|
|
}],
|
|
},
|
|
}, OtherFake.translate_ids({
|
|
'objectType': 'activity',
|
|
'verb': 'post',
|
|
'object': {
|
|
'id': 'fake:reply',
|
|
'objectType': 'note',
|
|
'inReplyTo': 'fake:post',
|
|
'author': {'id': 'fake:alice'},
|
|
'tags': [{
|
|
'objectType': 'mention',
|
|
'url': 'fake:bob',
|
|
}],
|
|
},
|
|
}))
|
|
|
|
def test_translate_ids_update_profile(self):
|
|
self.assert_equals({
|
|
'objectType': 'activity',
|
|
'verb': 'update',
|
|
'actor': 'other:u:fake:alice',
|
|
'object': {
|
|
'objectType': 'person',
|
|
'id': 'other:u:fake:alice',
|
|
},
|
|
}, OtherFake.translate_ids({
|
|
'objectType': 'activity',
|
|
'verb': 'update',
|
|
'actor': 'fake:alice',
|
|
'object': {
|
|
'objectType': 'person',
|
|
'id': 'fake:alice',
|
|
},
|
|
}))
|
|
|
|
def test_translate_ids_delete_actor(self):
|
|
self.assert_equals({
|
|
'objectType': 'activity',
|
|
'verb': 'delete',
|
|
'actor': 'other:u:fake:alice',
|
|
'object': 'other:u:fake:alice',
|
|
}, OtherFake.translate_ids({
|
|
'objectType': 'activity',
|
|
'verb': 'delete',
|
|
'actor': 'fake:alice',
|
|
'object': 'fake:alice',
|
|
}))
|
|
|
|
def test_translate_ids_attachments_mention_tags(self):
|
|
self.assert_equals({
|
|
'objectType': 'note',
|
|
'attachments': [
|
|
{'id': 'other:o:fa:fake:123'},
|
|
{'id': 'other:o:fa:fake:456',
|
|
'url': 'fake:456'},
|
|
],
|
|
'tags': [
|
|
{'objectType': 'mention', 'url': 'uri:other:u:fake:alice'},
|
|
{'url': 'fake:000'},
|
|
],
|
|
}, OtherFake.translate_ids({
|
|
'objectType': 'note',
|
|
'attachments': [
|
|
{'id': 'fake:123'},
|
|
{'url': 'fake:456'},
|
|
],
|
|
'tags': [
|
|
{'objectType': 'mention', 'url': 'fake:alice'},
|
|
{'url': 'fake:000'},
|
|
],
|
|
}))
|
|
|
|
def test_translate_ids_attachment_url_blocklisted(self):
|
|
self.assert_equals({
|
|
'objectType': 'note',
|
|
'attachments': [{'url': 'https://t.co/foo'}],
|
|
}, OtherFake.translate_ids({
|
|
'objectType': 'note',
|
|
'attachments': [{'url': 'https://t.co/foo'}],
|
|
}))
|
|
|
|
def test_translate_ids_copies(self):
|
|
self.store_object(id='fake:post',
|
|
copies=[Target(uri='other:post', protocol='other')])
|
|
self.make_user('other:user', cls=OtherFake,
|
|
copies=[Target(uri='fake:user', protocol='fake')])
|
|
|
|
self.assert_equals({
|
|
'objectType': 'activity',
|
|
'verb': 'post',
|
|
'actor': 'other:user',
|
|
'object': {
|
|
'id': 'other:o:fa:fake:reply',
|
|
'inReplyTo': 'other:post',
|
|
},
|
|
'attachments': [{
|
|
'objectType': 'note',
|
|
'id': 'other:post',
|
|
'url': 'fake:post',
|
|
}],
|
|
}, OtherFake.translate_ids({
|
|
'objectType': 'activity',
|
|
'verb': 'post',
|
|
'actor': 'fake:user',
|
|
'object': {
|
|
'id': 'fake:reply',
|
|
'inReplyTo': 'fake:post',
|
|
},
|
|
'attachments': [{
|
|
'objectType': 'note',
|
|
'url': 'fake:post',
|
|
}],
|
|
}))
|
|
|
|
def test_translate_ids_multiple_objects(self):
|
|
self.assert_equals({
|
|
'objectType': 'activity',
|
|
'verb': 'flag',
|
|
'object': [
|
|
'other:eve',
|
|
'other:o:fa:fake:bob',
|
|
],
|
|
}, OtherFake.translate_ids({
|
|
'objectType': 'activity',
|
|
'verb': 'flag',
|
|
'object': [
|
|
'other:eve',
|
|
'fake:bob',
|
|
]
|
|
}))
|
|
|
|
def test_translate_ids_inner_object_object_id(self):
|
|
# https://github.com/snarfed/bridgy-fed/issues/1492
|
|
self.assert_equals({
|
|
'object': {
|
|
'verb': 'flag',
|
|
'object': 'other:o:fa:fake:post',
|
|
},
|
|
}, OtherFake.translate_ids({
|
|
'object': {
|
|
'verb': 'flag',
|
|
'object': 'fake:post',
|
|
},
|
|
}))
|
|
|
|
def test_translate_ids_inner_object_actor_id(self):
|
|
# https://github.com/snarfed/bridgy-fed/issues/1492
|
|
self.assert_equals({
|
|
'object': {
|
|
'verb': 'follow',
|
|
'object': 'other:u:fake:bob',
|
|
},
|
|
}, OtherFake.translate_ids({
|
|
'object': {
|
|
'verb': 'follow',
|
|
'object': 'fake:bob',
|
|
},
|
|
}))
|
|
|
|
def test_translate_ids_featured_items(self):
|
|
self.assert_equals({
|
|
'featured': {
|
|
'type': 'OrderedCollection',
|
|
'orderedItems': [
|
|
'other:o:fa:fake:a',
|
|
'other:o:fa:fake:b',
|
|
],
|
|
},
|
|
}, OtherFake.translate_ids({
|
|
'featured': {
|
|
'type': 'OrderedCollection',
|
|
'orderedItems': [
|
|
'fake:a',
|
|
'fake:b',
|
|
],
|
|
},
|
|
}))
|
|
|
|
def test_translate_ids_single_featured_item(self):
|
|
self.assert_equals({
|
|
'featured': {
|
|
'type': 'OrderedCollection',
|
|
'orderedItems': [
|
|
'other:o:fa:fake:b',
|
|
],
|
|
},
|
|
}, OtherFake.translate_ids({
|
|
'featured': {
|
|
'type': 'OrderedCollection',
|
|
'orderedItems': [
|
|
'fake:b',
|
|
],
|
|
},
|
|
}))
|
|
|
|
def test_translate_ids_to_cc(self):
|
|
self.assert_equals({
|
|
'id': 'xyz',
|
|
'to': ['other:u:fake:alice', 'other:bob'],
|
|
'cc': ['other:u:efake:eve', as2.PUBLIC_AUDIENCE],
|
|
}, OtherFake.translate_ids({
|
|
'id': 'xyz',
|
|
'to': ['fake:alice', 'other:bob'],
|
|
'cc': ['efake:eve', as2.PUBLIC_AUDIENCE],
|
|
}))
|
|
|
|
def test_translate_ids_empty(self):
|
|
self.assertEqual({}, Fake.translate_ids({}))
|
|
|
|
def test_translate_ids_single_inReplyTo(self):
|
|
obj = {'inReplyTo': 'foo'}
|
|
self.assertEqual(obj, Fake.translate_ids(obj))
|
|
|
|
def test_translate_ids_multiple_inReplyTo(self):
|
|
obj = {'inReplyTo': ['foo', 'bar']}
|
|
self.assertEqual(obj, Fake.translate_ids(obj))
|
|
|
|
def test_convert_object_is_from_user_adds_source_links(self):
|
|
alice = Fake(id='fake:alice')
|
|
self.assertEqual({
|
|
'objectType': 'person',
|
|
'id': 'other:u:fake:alice',
|
|
'url': 'http://unused',
|
|
'summary': 'something about me<br><br>🌉 <a href="https://fed.brid.gy/fa/fake:handle:alice">bridged</a> from <a href="web:fake:alice">fake:handle:alice</a> on fake-phrase by <a href="https://fed.brid.gy/">Bridgy Fed</a>',
|
|
}, OtherFake.convert(Object(
|
|
id='fake:profile:alice', source_protocol='fake', our_as1={
|
|
'objectType': 'person',
|
|
'id': 'fake:alice',
|
|
'url': 'http://unused',
|
|
'summary': 'something about me',
|
|
}), from_user=alice))
|
|
|
|
def test_convert_object_isnt_from_user_adds_source_links(self):
|
|
bob = Fake(id='fake:bob')
|
|
self.assertEqual({
|
|
'objectType': 'person',
|
|
'id': 'other:u:fake:alice',
|
|
'url': 'http://al/ice',
|
|
'summary': '🌉 bridged from <a href="http://al/ice">al/ice</a> on fake-phrase by <a href="https://fed.brid.gy/">Bridgy Fed</a>',
|
|
}, OtherFake.convert(Object(id='fake:alice', source_protocol='fake', our_as1={
|
|
'objectType': 'person',
|
|
'id': 'fake:alice',
|
|
'url': 'http://al/ice',
|
|
}), from_user=bob))
|
|
|
|
def test_convert_actor_without_from_user_doesnt_add_source_links(self):
|
|
self.assertEqual({
|
|
'objectType': 'person',
|
|
'id': 'other:u:fake:alice',
|
|
'url': 'http://al/ice',
|
|
}, OtherFake.convert(Object(id='fake:alice', source_protocol='fake', our_as1={
|
|
'objectType': 'person',
|
|
'id': 'fake:alice',
|
|
'url': 'http://al/ice',
|
|
})))
|
|
|
|
def test_convert_doesnt_duplicate_source_links(self):
|
|
alice = Fake(id='fake:alice')
|
|
summary = 'something about me<br><br>🌉 bridged from <a href="http://al/ice">someone else</a> by <a href="https://fed.brid.gy/">Bridgy Fed</a>'
|
|
self.assertEqual({
|
|
'objectType': 'person',
|
|
'id': 'other:u:fake:alice',
|
|
'summary': summary,
|
|
}, OtherFake.convert(Object(id='fake:alice', source_protocol='fake', our_as1={
|
|
'objectType': 'person',
|
|
'id': 'fake:alice',
|
|
'summary': summary,
|
|
}), from_user=alice))
|
|
|
|
def test_convert_object_adds_source_links_to_create_update(self):
|
|
alice = Fake(id='fake:alice')
|
|
for verb in 'post', 'update':
|
|
self.assertEqual({
|
|
'objectType': 'activity',
|
|
'verb': verb,
|
|
'id': 'other:o:fa:fake:profile:update',
|
|
'object': {
|
|
'objectType': 'person',
|
|
'id': 'other:u:fake:profile:alice',
|
|
'summary': 'something about me<br><br>🌉 <a href="https://fed.brid.gy/fa/fake:handle:alice">bridged</a> from <a href="web:fake:alice">fake:handle:alice</a> on fake-phrase by <a href="https://fed.brid.gy/">Bridgy Fed</a>',
|
|
},
|
|
}, OtherFake.convert(
|
|
Object(id='fake:profile:update', source_protocol='fake', our_as1={
|
|
'objectType': 'activity',
|
|
'verb': verb,
|
|
'object': {
|
|
'id': 'fake:profile:alice',
|
|
'objectType': 'person',
|
|
'summary': 'something about me',
|
|
},
|
|
}), from_user=alice))
|
|
|
|
def test_check_supported(self):
|
|
for obj in (
|
|
{'objectType': 'note', 'content': 'foo'},
|
|
{'objectType': 'note', 'attachments': {'stream': {'url': 'http://x/yz'}}},
|
|
{'objectType': 'note', 'image': [{'url': 'http://my/pic'}]},
|
|
{'objectType': 'activity', 'verb': 'post',
|
|
'object': {'objectType': 'note', 'content': 'foo'}},
|
|
{'objectType': 'activity', 'verb': 'follow'},
|
|
{'objectType': 'activity', 'verb': 'delete', 'object': 'x'},
|
|
{'objectType': 'activity', 'verb': 'undo', 'object': {'foo': 'bar'}},
|
|
{'objectType': 'activity', 'verb': 'undo',
|
|
'object': {'objectType': 'activity', 'verb': 'share'}},
|
|
{'objectType': 'activity', 'verb': 'flag'},
|
|
):
|
|
with self.subTest(obj=obj):
|
|
Fake.check_supported(Object(our_as1=obj))
|
|
|
|
for obj in (
|
|
{'objectType': 'event'},
|
|
{'objectType': 'activity', 'verb': 'post',
|
|
'object': {'objectType': 'event'}},
|
|
):
|
|
with self.subTest(obj=obj), self.assertRaises(NoContent):
|
|
Fake.check_supported(Object(our_as1=obj))
|
|
|
|
# Fake doesn't support DMs, ExplicitFake does
|
|
for author, recip in (
|
|
('ap.brid.gy', 'did:bob'),
|
|
('did:bob', 'ap.brid.gy'),
|
|
):
|
|
bot_dm = Object(our_as1={
|
|
'objectType': 'note',
|
|
'author': author,
|
|
'to': [recip],
|
|
'content': 'hello world',
|
|
})
|
|
ExplicitFake.check_supported(bot_dm)
|
|
with self.assertRaises(NoContent):
|
|
Fake.check_supported(bot_dm)
|
|
|
|
# not from or to a protocol bot user
|
|
dm = Object(our_as1={
|
|
'objectType': 'note',
|
|
'author': 'did:alice',
|
|
'to': ['did:bob'],
|
|
'content': 'hello world',
|
|
})
|
|
for proto in Fake, ExplicitFake:
|
|
with self.subTest(proto=proto), self.assertRaises(NoContent):
|
|
proto.check_supported(dm)
|
|
|
|
# blank content and no video/audio/image
|
|
for obj in (
|
|
{'objectType': 'note'},
|
|
{'objectType': 'note', 'content': ' '},
|
|
{'objectType': 'note', 'content': '<p></p>'},
|
|
{'objectType': 'activity', 'verb': 'post',
|
|
'object': {'objectType': 'note'}},
|
|
{'objectType': 'activity', 'verb': 'update',
|
|
'object': {'objectType': 'note'}},
|
|
):
|
|
with self.subTest(obj=obj), self.assertRaises(NoContent):
|
|
Fake.check_supported(Object(our_as1=obj))
|
|
|
|
# from and to a copy id of a protocol bot user
|
|
self.make_user(cls=Web, id='ap.brid.gy',
|
|
copies=[Target(protocol='fake', uri='fake:ap-bot')])
|
|
common.protocol_user_copy_ids.cache_clear()
|
|
dm.our_as1['author'] = 'fake:ap-bot'
|
|
proto.check_supported(dm)
|
|
|
|
dm.our_as1.update({
|
|
'author': 'did:alice',
|
|
'to': ['fake:ap-bot'],
|
|
})
|
|
proto.check_supported(dm)
|
|
|
|
def test_bot_follow(self):
|
|
self.make_user(id='fa.brid.gy', cls=Web)
|
|
user = self.make_user(id='fake:user', cls=Fake, obj_id='fake:user')
|
|
Fake.bot_follow(user)
|
|
|
|
self.assertEqual([('fake:user:target', {
|
|
'objectType': 'activity',
|
|
'verb': 'follow',
|
|
'id': 'https://fa.brid.gy/#follow-back-fake:user-2022-01-02T03:04:05+00:00',
|
|
'actor': 'fa.brid.gy',
|
|
'object': 'fake:user',
|
|
})], Fake.sent)
|
|
|
|
def test_bot_follow_user_missing_obj(self):
|
|
self.make_user(id='fa.brid.gy', cls=Web)
|
|
user = Fake(id='fake:user')
|
|
assert not user.obj
|
|
Fake.bot_follow(user)
|
|
self.assertEqual([], Fake.sent)
|
|
|
|
def test_check_can_migrate_out(self, *_):
|
|
fake = Fake(id='fake:user', enabled_protocols=['other'])
|
|
OtherFake.check_can_migrate_out(fake, 'other:user')
|
|
|
|
def test_check_can_migrate_out_bad_user_id(self, *_):
|
|
fake = Fake(id='fake:user', enabled_protocols=['other'])
|
|
with self.assertRaises(ValueError):
|
|
OtherFake.check_can_migrate_out(self.user, 'at://did:xyz')
|
|
|
|
def test_check_can_migrate_out_user_not_enabled(self, *_):
|
|
fake = Fake(id='fake:user', enabled_protocols=['efake'])
|
|
with self.assertRaises(ValueError):
|
|
OtherFake.check_can_migrate_out(fake, 'https://in.st/eve')
|
|
|
|
def test_check_can_migrate_out_same_protocol(self, *_):
|
|
fake = Fake(id='fake:user', enabled_protocols=['other'])
|
|
with self.assertRaises(ValueError):
|
|
Fake.check_can_migrate_out(fake, 'fake:eve')
|
|
|
|
class ProtocolReceiveTest(TestCase):
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.user = self.make_user('fake:user', cls=Fake, obj_id='fake:user')
|
|
self.alice = self.make_user('other:alice', cls=OtherFake, obj_id='other:alice')
|
|
self.bob = self.make_user('other:bob', cls=OtherFake, obj_id='other:bob')
|
|
|
|
def assert_object(self, id, **props):
|
|
props.setdefault('source_protocol', 'fake')
|
|
return super().assert_object(id, **props)
|
|
|
|
def make_followers(self):
|
|
Follower.get_or_create(to=self.user, from_=self.alice)
|
|
Follower.get_or_create(to=self.user, from_=self.bob)
|
|
Follower.get_or_create(to=self.user, from_=OtherFake(id='other:eve'),
|
|
status='inactive')
|
|
|
|
def test_create_post(self):
|
|
self.user.enabled_protocols = ['efake']
|
|
self.user.put()
|
|
|
|
self.make_followers()
|
|
|
|
eve = self.make_user('efake:eve', cls=ExplicitFake, obj_id='efake:eve')
|
|
Follower.get_or_create(to=self.user, from_=eve)
|
|
|
|
post_as1 = {
|
|
'id': 'fake:post',
|
|
'objectType': 'note',
|
|
'content': 'foo',
|
|
}
|
|
create_as1 = {
|
|
'id': 'fake:create',
|
|
'objectType': 'activity',
|
|
'verb': 'post',
|
|
'actor': 'fake:user',
|
|
'object': post_as1,
|
|
}
|
|
self.assertEqual(('OK', 202), Fake.receive_as1(create_as1))
|
|
|
|
self.assert_object('fake:post',
|
|
our_as1=post_as1,
|
|
type='note',
|
|
copies=[
|
|
Target(protocol='efake', uri='efake:o:fa:fake:post'),
|
|
Target(protocol='other', uri='other:o:fa:fake:post'),
|
|
],
|
|
feed=[eve.key],
|
|
users=[self.user.key],
|
|
deleted=False,
|
|
)
|
|
self.assertIsNone(Object.get_by_id('fake:create'))
|
|
self.assertEqual([
|
|
('other:alice:target', create_as1),
|
|
('other:bob:target', create_as1),
|
|
], OtherFake.sent)
|
|
|
|
def test_create_post_object_missing_id(self):
|
|
self.make_followers()
|
|
|
|
# got an activity like this from Pandacap
|
|
# https://github.com/IsaacSchemm/Pandacap
|
|
create_as1 = {
|
|
'id': 'fake:create',
|
|
'objectType': 'activity',
|
|
'verb': 'post',
|
|
'actor': 'fake:user',
|
|
'object': [{
|
|
'Item1': 'id',
|
|
'Item2': 'https://pandacap.azurewebsites.net/#transient-abc-123',
|
|
}],
|
|
}
|
|
with self.assertRaises(ErrorButDoNotRetryTask):
|
|
Fake.receive_as1(create_as1)
|
|
|
|
self.assertEqual([], OtherFake.sent)
|
|
|
|
def test_create_post_bare_object(self):
|
|
self.make_followers()
|
|
|
|
post_as1 = {
|
|
'id': 'fake:post',
|
|
'objectType': 'note',
|
|
'author': 'fake:user',
|
|
'content': 'foo',
|
|
}
|
|
self.assertEqual(('OK', 202), Fake.receive_as1(post_as1))
|
|
|
|
self.assert_object(
|
|
'fake:post',
|
|
our_as1=post_as1,
|
|
type='note',
|
|
copies=[Target(protocol='other', uri='other:o:fa:fake:post')],
|
|
users=[Fake(id='fake:user').key],
|
|
deleted=False,
|
|
)
|
|
|
|
self.assertIsNone(Object.get_by_id('fake:post#bridgy-fed-create'))
|
|
self.assertEqual('other:alice:target', OtherFake.sent[0][0])
|
|
self.assertEqual('other:bob:target', OtherFake.sent[1][0])
|
|
self.assertEqual('fake:post#bridgy-fed-create', OtherFake.sent[0][1]['id'])
|
|
self.assertEqual('fake:post#bridgy-fed-create', OtherFake.sent[1][1]['id'])
|
|
|
|
@patch.object(ATProto, 'send', return_value=True)
|
|
def test_post_by_user_enabled_atproto_adds_pds_target(self, mock_send):
|
|
self.user.enabled_protocols = ['atproto']
|
|
self.user.put()
|
|
|
|
self.assertEqual(('OK', 202), Fake.receive_as1({
|
|
'id': 'fake:post',
|
|
'objectType': 'note',
|
|
'author': 'fake:user',
|
|
'content': 'foo',
|
|
}))
|
|
|
|
self.assertEqual(1, mock_send.call_count)
|
|
[obj, url], _ = mock_send.call_args
|
|
self.assertEqual('fake:post#bridgy-fed-create', obj.key.id())
|
|
self.assertEqual(ATProto.DEFAULT_TARGET, url)
|
|
|
|
def test_post_not_public_ignored(self):
|
|
self.assertEqual(('OK', 200), Fake.receive_as1({
|
|
'id': 'fake:post',
|
|
'objectType': 'note',
|
|
'author': 'fake:user',
|
|
'to': ['fake:user/followers'],
|
|
}))
|
|
self.assertIsNone(Object.get_by_id('fake:post'))
|
|
|
|
def test_post_unlisted_ignored(self):
|
|
self.assertEqual(('OK', 200), Fake.receive_as1({
|
|
'id': 'fake:post',
|
|
'objectType': 'note',
|
|
'author': 'fake:user',
|
|
'to': ['@unlisted'],
|
|
}))
|
|
self.assertIsNone(Object.get_by_id('fake:post'))
|
|
|
|
@patch.object(ATProto, 'send')
|
|
def test_reply_to_not_bridged_account_skips_atproto(self, mock_send):
|
|
user = self.make_user('efake:user', cls=ExplicitFake,
|
|
enabled_protocols=['atproto'])
|
|
|
|
self.eve = self.make_user('efake:eve', cls=ExplicitFake)
|
|
self.store_object(id='efake:post', our_as1={
|
|
'id': 'efake:post',
|
|
'objectType': 'note',
|
|
'author': 'efake:eve',
|
|
})
|
|
|
|
ExplicitFake.receive_as1({
|
|
'id': 'efake:reply',
|
|
'objectType': 'note',
|
|
'author': 'efake:user',
|
|
'inReplyTo': 'efake:post',
|
|
'content': 'foo',
|
|
})
|
|
|
|
self.assertEqual(0, mock_send.call_count)
|
|
|
|
@patch.object(ATProto, 'send')
|
|
def test_reply_to_non_bridged_post_with_mention_skips_atproto(self, mock_send):
|
|
self.user.enabled_protocols = ['atproto']
|
|
self.user.put()
|
|
|
|
self.store_object(id='fake:post', our_as1={
|
|
'id': 'fake:post',
|
|
'objectType': 'note',
|
|
'author': 'other:alice',
|
|
})
|
|
|
|
Fake.receive_as1({
|
|
'id': 'fake:reply',
|
|
'objectType': 'note',
|
|
'actor': 'fake:user',
|
|
'inReplyTo': 'fake:post',
|
|
'content': 'foo',
|
|
'tags': [{
|
|
'objectType': 'mention',
|
|
'url': 'other:bob'
|
|
}],
|
|
})
|
|
|
|
self.assertEqual(0, mock_send.call_count)
|
|
|
|
def test_reply_to_non_bridged_post_skips_enabled_protocol_with_followers(self):
|
|
self.make_user(id='fa.brid.gy', cls=Web)
|
|
|
|
# should skip even if it's enabled and we have followers there
|
|
self.user.enabled_protocols = ['efake']
|
|
self.user.put()
|
|
|
|
eve = self.make_user('efake:eve', cls=ExplicitFake)
|
|
Follower.get_or_create(from_=eve, to=self.user)
|
|
|
|
self.store_object(id='fake:post', source_protocol='fake', our_as1={
|
|
'id': 'fake:post',
|
|
'objectType': 'note',
|
|
'author': 'fake:alice',
|
|
})
|
|
_, code = Fake.receive_as1({
|
|
'id': 'fake:reply',
|
|
'objectType': 'note',
|
|
'actor': 'fake:user',
|
|
'inReplyTo': 'fake:post',
|
|
'content': 'foo',
|
|
})
|
|
self.assertEqual(204, code)
|
|
self.assertEqual([], ExplicitFake.sent)
|
|
self.assertEqual([], Fake.sent)
|
|
|
|
def test_reply_from_non_bridged_user_isnt_bridged_gets_dm_prompt_and_notif(self):
|
|
self.make_user(id='other.brid.gy', cls=Web)
|
|
self.make_user(id='efake.brid.gy', cls=Web)
|
|
|
|
user = self.make_user('other:user', cls=OtherFake, enabled_protocols=['efake'],
|
|
obj_as1={'id': 'other:user'})
|
|
eve = self.make_user('efake:eve', cls=ExplicitFake,
|
|
obj_as1={'id': 'efake:eve'})
|
|
|
|
self.store_object(id='other:post', source_protocol='other', our_as1={
|
|
'id': 'other:post',
|
|
'objectType': 'note',
|
|
'author': 'other:user',
|
|
})
|
|
|
|
_, code = ExplicitFake.receive_as1({
|
|
'id': 'efake:reply',
|
|
'url': 'http://efake/reply',
|
|
'objectType': 'note',
|
|
'actor': 'efake:eve',
|
|
'inReplyTo': 'other:post',
|
|
'content': 'foo',
|
|
})
|
|
self.assertEqual(204, code)
|
|
|
|
# check that we sent a prompt to eve and a notif to other:user
|
|
test_dms.DmsTest().assert_sent(OtherFake, eve, 'replied_to_bridged_user', """Hi! You <a href="http://efake/reply">recently replied to</a> <a class="h-card u-author" rel="me" href="web:other:user" title="other:handle:user">other:handle:user</a>, who's bridged here from other-phrase. If you want them to see your replies, you can bridge your account into other-phrase by following this account. <a href="https://fed.brid.gy/docs">See the docs</a> for more information.""")
|
|
test_dms.DmsTest().assert_sent(ExplicitFake, user, '?', """\
|
|
<p>Hi! Here are your recent interactions from people who aren't bridged into other-phrase:
|
|
<ul>
|
|
<li><a href="http://efake/reply">efake/reply</a>
|
|
</ul>""")
|
|
|
|
def test_quote_from_non_bridged_user_isnt_bridged_gets_dm_prompt_and_notif(self):
|
|
self.make_user(id='other.brid.gy', cls=Web)
|
|
self.make_user(id='efake.brid.gy', cls=Web)
|
|
|
|
user = self.make_user('other:user', cls=OtherFake, enabled_protocols=['efake'],
|
|
obj_as1={'id': 'other:user'})
|
|
eve = self.make_user('efake:eve', cls=ExplicitFake,
|
|
obj_as1={'id': 'efake:eve'})
|
|
|
|
self.store_object(id='other:post', source_protocol='other', our_as1={
|
|
'id': 'other:post',
|
|
'objectType': 'note',
|
|
'author': 'other:user',
|
|
})
|
|
_, code = ExplicitFake.receive_as1({
|
|
'objectType': 'note',
|
|
'id': 'efake:quote',
|
|
'url': 'http://efake/quote',
|
|
'author': 'efake:eve',
|
|
'content': 'weird flex but ok',
|
|
'attachments': [{
|
|
'objectType': 'note',
|
|
'id': 'other:post',
|
|
'url': 'http://other/post',
|
|
}],
|
|
})
|
|
self.assertEqual(204, code)
|
|
|
|
# check that we sent a prompt to eve and a notif to other:user
|
|
test_dms.DmsTest().assert_sent(OtherFake, eve, 'replied_to_bridged_user', """Hi! You <a href="http://efake/quote">recently quoted</a> <a class="h-card u-author" rel="me" href="web:other:user" title="other:handle:user">other:handle:user</a>, who's bridged here from other-phrase. If you want them to see your quotes, you can bridge your account into other-phrase by following this account. <a href="https://fed.brid.gy/docs">See the docs</a> for more information.""")
|
|
test_dms.DmsTest().assert_sent(ExplicitFake, user, '?', """\
|
|
<p>Hi! Here are your recent interactions from people who aren't bridged into other-phrase:
|
|
<ul>
|
|
<li><a href="http://efake/quote">efake/quote</a>
|
|
</ul>""")
|
|
|
|
def test_mention_non_bridged_user_isnt_bridged_gets_dm_prompt_and_notif(self):
|
|
self.make_user(id='other.brid.gy', cls=Web)
|
|
self.make_user(id='efake.brid.gy', cls=Web)
|
|
|
|
user = self.make_user('other:user', cls=OtherFake, enabled_protocols=['efake'],
|
|
obj_as1={'id': 'other:user'})
|
|
eve = self.make_user('efake:eve', cls=ExplicitFake,
|
|
obj_as1={'id': 'efake:eve'})
|
|
|
|
_, code = ExplicitFake.receive_as1({
|
|
'objectType': 'note',
|
|
'id': 'efake:mention',
|
|
'url': 'http://efake/mention',
|
|
'author': 'efake:eve',
|
|
'content': 'weird flex but ok',
|
|
'tags': [{
|
|
'objectType': 'mention',
|
|
'url': 'other:user',
|
|
}],
|
|
})
|
|
self.assertEqual(204, code)
|
|
|
|
# check that we sent a prompt to eve and a notif to other:user
|
|
test_dms.DmsTest().assert_sent(OtherFake, eve, 'replied_to_bridged_user', """Hi! You <a href="http://efake/mention">recently mentioned</a> <a class="h-card u-author" rel="me" href="web:other:user" title="other:handle:user">other:handle:user</a>, who's bridged here from other-phrase. If you want them to see your mentions, you can bridge your account into other-phrase by following this account. <a href="https://fed.brid.gy/docs">See the docs</a> for more information.""")
|
|
test_dms.DmsTest().assert_sent(ExplicitFake, user, '?', """\
|
|
<p>Hi! Here are your recent interactions from people who aren't bridged into other-phrase:
|
|
<ul>
|
|
<li><a href="http://efake/mention">efake/mention</a>
|
|
</ul>""")
|
|
|
|
@patch.object(ATProto, 'send', return_value=True)
|
|
def test_repost_of_non_bridged_account_skips_atproto(self, mock_send):
|
|
user = self.make_user('efake:user', cls=ExplicitFake,
|
|
enabled_protocols=['atproto'])
|
|
|
|
self.eve = self.make_user('efake:eve', cls=ExplicitFake)
|
|
self.store_object(id='efake:post', our_as1={
|
|
'id': 'efake:post',
|
|
'objectType': 'note',
|
|
'author': 'efake:eve',
|
|
})
|
|
|
|
_, code = ExplicitFake.receive_as1({
|
|
'id': 'efake:repost',
|
|
'objectType': 'activity',
|
|
'verb': 'share',
|
|
'actor': 'efake:user',
|
|
'object': 'efake:post',
|
|
})
|
|
self.assertEqual(204, code)
|
|
self.assertEqual(0, mock_send.call_count)
|
|
|
|
@patch.object(ATProto, 'send', return_value=True)
|
|
def test_repost_of_not_bridged_post_skips_atproto(self, mock_send):
|
|
user = self.make_user('efake:user', cls=ExplicitFake,
|
|
enabled_protocols=['atproto'])
|
|
|
|
self.eve = self.make_user('efake:eve', cls=ExplicitFake,
|
|
enabled_protocols=['atproto'])
|
|
self.store_object(id='efake:post', our_as1={
|
|
'id': 'efake:post',
|
|
'objectType': 'note',
|
|
'author': 'efake:eve',
|
|
})
|
|
|
|
_, code = ExplicitFake.receive_as1({
|
|
'id': 'efake:repost',
|
|
'objectType': 'activity',
|
|
'verb': 'share',
|
|
'actor': 'efake:user',
|
|
'object': 'efake:post',
|
|
})
|
|
self.assertEqual(204, code)
|
|
self.assertEqual(0, mock_send.call_count)
|
|
|
|
def test_repost_of_not_bridged_post_skips_enabled_protocol_with_followers(self):
|
|
# should skip even if it's enabled and we have followers there
|
|
self.user.enabled_protocols = ['efake']
|
|
self.user.put()
|
|
|
|
eve = self.make_user('efake:eve', cls=ExplicitFake)
|
|
Follower.get_or_create(from_=eve, to=self.user)
|
|
|
|
self.store_object(id='fake:post', source_protocol='fake', our_as1={
|
|
'id': 'fake:post',
|
|
'objectType': 'note',
|
|
'author': 'fake:alice',
|
|
})
|
|
_, code = Fake.receive_as1({
|
|
'id': 'fake:repost',
|
|
'objectType': 'activity',
|
|
'verb': 'share',
|
|
'actor': 'fake:user',
|
|
'object': 'fake:post',
|
|
})
|
|
self.assertEqual(204, code)
|
|
self.assertEqual([], ExplicitFake.sent)
|
|
self.assertEqual([], Fake.sent)
|
|
|
|
def test_undo_repost(self):
|
|
self.make_followers()
|
|
|
|
self.store_object(id='other:orig', source_protocol='other', our_as1={
|
|
'objectType': 'note',
|
|
'id': 'other:orig',
|
|
'actor': 'other:user',
|
|
})
|
|
self.store_object(id='fake:share', source_protocol='fake', our_as1={
|
|
'objectType': 'activity',
|
|
'verb': 'share',
|
|
'id': 'fake:share',
|
|
'actor': 'fake:user',
|
|
'object': 'other:orig',
|
|
})
|
|
|
|
undo_as1 = {
|
|
'objectType': 'activity',
|
|
'verb': 'undo',
|
|
'id': 'fake:undo',
|
|
'actor': 'fake:user',
|
|
'object': 'fake:share',
|
|
}
|
|
self.assertEqual(('OK', 202), Fake.receive_as1(undo_as1))
|
|
self.assertTrue(Object.get_by_id('fake:share').deleted)
|
|
self.assertEqual([
|
|
('other:alice:target', undo_as1),
|
|
('other:bob:target', undo_as1),
|
|
('other:orig:target', undo_as1),
|
|
], OtherFake.sent)
|
|
|
|
@patch.object(ATProto, 'send', return_value=True)
|
|
def test_follow_of_bridged_account_by_not_bridged_account_skips_atproto(
|
|
self, mock_send):
|
|
user = self.make_user('efake:user', cls=ExplicitFake)
|
|
self.store_object(id='did:plc:eve', raw=DID_DOC)
|
|
eve = self.make_user('did:plc:eve', cls=ATProto, enabled_protocols=['efake'],
|
|
copies=[Target(uri='efake:eve', protocol='efake')],
|
|
obj_bsky=ACTOR_PROFILE_BSKY)
|
|
|
|
_, code = ExplicitFake.receive_as1({
|
|
'id': 'efake:follow',
|
|
'objectType': 'activity',
|
|
'verb': 'follow',
|
|
'actor': 'efake:user',
|
|
'object': 'efake:eve',
|
|
})
|
|
self.assertEqual(204, code)
|
|
|
|
self.assert_entities_equal(Follower(from_=user.key, to=eve.key,
|
|
follow=Object(id='efake:follow').key),
|
|
Follower.query().fetch(),
|
|
ignore=['created', 'updated'])
|
|
self.assertEqual(0, mock_send.call_count)
|
|
|
|
def test_targets_block(self):
|
|
self.bob.obj.our_as1 = {'foo': 'bar'}
|
|
self.bob.obj.put()
|
|
|
|
block = {
|
|
'objectType': 'activity',
|
|
'verb': 'block',
|
|
'id': 'other:block',
|
|
'actor': 'other:alice',
|
|
'object': 'other:bob',
|
|
}
|
|
self.assertEqual(
|
|
[Target(uri='other:bob:target', protocol='other')],
|
|
list(Fake.targets(Object(our_as1=block), from_user=self.user).keys()))
|
|
|
|
def test_targets_undo_composite_block(self):
|
|
self.bob.obj.our_as1 = {'foo': 'bar'}
|
|
self.bob.obj.put()
|
|
|
|
obj = Object(id='other:undo', our_as1={
|
|
'objectType': 'activity',
|
|
'verb': 'undo',
|
|
'id': 'other:undo',
|
|
'actor': 'fake:user',
|
|
'object': {
|
|
'objectType': 'activity',
|
|
'verb': 'block',
|
|
'id': 'other:block',
|
|
'actor': 'fake:user',
|
|
'object': 'other:bob',
|
|
},
|
|
})
|
|
self.assertEqual(
|
|
[Target(uri='other:bob:target', protocol='other')],
|
|
list(Fake.targets(obj, from_user=self.user).keys()))
|
|
|
|
def test_targets_undo_block_id(self):
|
|
self.bob.obj.our_as1 = {'foo': 'bar'}
|
|
self.bob.obj.put()
|
|
|
|
self.store_object(id='fake:block', our_as1={
|
|
'objectType': 'activity',
|
|
'verb': 'block',
|
|
'id': 'fake:block',
|
|
'actor': 'fake:user',
|
|
'object': 'other:bob',
|
|
})
|
|
|
|
obj = Object(id='fake:undo', our_as1={
|
|
'objectType': 'activity',
|
|
'verb': 'undo',
|
|
'id': 'fake:undo',
|
|
'actor': 'fake:user',
|
|
'object': 'fake:block',
|
|
})
|
|
self.assertEqual(
|
|
[Target(uri='other:bob:target', protocol='other')],
|
|
list(Fake.targets(obj, from_user=self.user).keys()))
|
|
|
|
def test_targets_undo_share_composite(self):
|
|
self.make_followers()
|
|
|
|
OtherFake.fetchable['other:orig'] = {
|
|
'objectType': 'note',
|
|
'id': 'other:orig',
|
|
}
|
|
|
|
share = {
|
|
'objectType': 'activity',
|
|
'verb': 'share',
|
|
'id': 'fake:share',
|
|
'actor': 'fake:user',
|
|
'object': 'other:orig',
|
|
}
|
|
Fake.fetchable['fake:share'] = share
|
|
|
|
obj = Object(id='fake:undo', our_as1={
|
|
'objectType': 'activity',
|
|
'verb': 'undo',
|
|
'actor': 'fake:user',
|
|
'object': share,
|
|
})
|
|
self.assertEqual({
|
|
Target(protocol='other', uri='other:alice:target'),
|
|
Target(protocol='other', uri='other:bob:target'),
|
|
Target(protocol='other', uri='other:orig:target'),
|
|
}, Fake.targets(obj, from_user=self.user).keys())
|
|
|
|
def test_targets_repost_of_quote_with_article_tag_uses_quote_post_as_orig_obj(self):
|
|
"""https://github.com/snarfed/bridgy-fed/issues/1357"""
|
|
self.make_followers()
|
|
|
|
self.user.enabled_protocols=['web']
|
|
self.user.put()
|
|
|
|
eve = self.make_user('eve.com', cls=Web)
|
|
web_link = self.store_object(id='http://eve.com/link', source_protocol='web',
|
|
our_as1={'foo': 'bar'})
|
|
|
|
quote_as1 = {
|
|
'objectType': 'note',
|
|
'id': 'fake:quote',
|
|
'author': 'fake:user',
|
|
'content': 'foo bar baz',
|
|
'tags': [{
|
|
'objectType': 'article',
|
|
'url': 'http://eve.com/link',
|
|
'displayName': 'bar',
|
|
'startIndex': 4,
|
|
'length': 3
|
|
}],
|
|
'attachments': [{
|
|
'objectType': 'note',
|
|
'id': 'fake:orig',
|
|
'url': 'url:fake:orig',
|
|
}]
|
|
}
|
|
quote_obj = self.store_object(
|
|
id='fake:quote',
|
|
source_protocol='fake',
|
|
our_as1=quote_as1,
|
|
copies=[Target(protocol='other', uri='other:quote')])
|
|
|
|
repost_as1 = {
|
|
'objectType': 'activity',
|
|
'verb': 'share',
|
|
'id': 'fake:repost',
|
|
'actor': 'fake:user',
|
|
'object': quote_as1,
|
|
}
|
|
targets = Fake.targets(Object(id='fake:repost', our_as1=repost_as1),
|
|
from_user=self.user)
|
|
self.assertEqual({
|
|
'other:quote:target': quote_obj.key.id(),
|
|
'other:alice:target': quote_obj.key.id(),
|
|
'other:bob:target': quote_obj.key.id(),
|
|
'http://eve.com/link': None,
|
|
}, {target.uri: obj.key.id() if obj else None for target, obj in targets.items()})
|
|
|
|
@patch.object(ATProto, 'send', return_value=True)
|
|
def test_atproto_targets_normalize_pds_url(self, mock_send):
|
|
# we were over-normalizing our PDS URL https://atproto.brid.gy , adding
|
|
# a trailing slash, and then ending up with both versions in targets.
|
|
# https://github.com/snarfed/bridgy-fed/issues/1032
|
|
self.user.enabled_protocols = ['atproto']
|
|
|
|
# atproto follower
|
|
self.store_object(id='did:plc:eve', raw={**DID_DOC, 'id': 'at://did:plc:eve'})
|
|
obj = self.store_object(id='at://did:plc:eve/app.bsky.actor.profile/self',
|
|
bsky=ACTOR_PROFILE_BSKY)
|
|
eve = self.make_user('did:plc:eve', cls=ATProto, obj_key=obj.key)
|
|
Follower.get_or_create(from_=eve, to=self.user)
|
|
|
|
note = Object(our_as1={
|
|
'id': 'fake:post',
|
|
'objectType': 'note',
|
|
'author': 'fake:user',
|
|
})
|
|
create = Object(id='fake:post', our_as1={
|
|
'objectType': 'activity',
|
|
'verb': 'post',
|
|
'object': note.as1,
|
|
})
|
|
self.assertEqual({
|
|
Target(uri='https://atproto.brid.gy', protocol='atproto'): None,
|
|
}, Fake.targets(create, crud_obj=note, from_user=self.user))
|
|
|
|
def test_create_post_dont_deliver_to_follower_if_protocol_isnt_enabled(self):
|
|
# user who hasn't enabled either Fake or OtherFake, so we shouldn't
|
|
# deliver to followers on those protocols
|
|
user = self.make_user('efake:user', cls=ExplicitFake,
|
|
obj_id='efake:user')
|
|
frank = self.make_user('other:frank', cls=OtherFake, obj_id='other:frank')
|
|
Follower.get_or_create(to=user, from_=self.alice)
|
|
Follower.get_or_create(to=user, from_=frank)
|
|
|
|
_, code = ExplicitFake.receive_as1({
|
|
'objectType': 'note',
|
|
'id': 'efake:post',
|
|
'author': 'efake:user',
|
|
'content': 'foo'
|
|
})
|
|
self.assertEqual(204, code)
|
|
|
|
self.assertEqual([], Fake.sent)
|
|
self.assertEqual([], OtherFake.sent)
|
|
obj = Object.get_by_id('efake:post#bridgy-fed-create')
|
|
|
|
def test_create_post_use_instead(self):
|
|
self.make_user('fake:not-this', cls=Fake, use_instead=self.user.key, obj_mf2={
|
|
'type': ['h-card'],
|
|
'properties': {
|
|
# this is the key part to test; Object.as1 uses this as id
|
|
'url': ['fake:user'],
|
|
},
|
|
})
|
|
self.make_followers()
|
|
|
|
note_as1 = {
|
|
'id': 'fake:post',
|
|
'objectType': 'note',
|
|
'author': 'fake:user',
|
|
'content': 'foo',
|
|
}
|
|
obj = self.store_object(id='fake:post', our_as1=note_as1,
|
|
source_protocol='fake')
|
|
|
|
self.assertEqual(('OK', 202), Fake.receive_as1(note_as1))
|
|
self.assertEqual(2, len(OtherFake.sent))
|
|
|
|
post_as1 = {
|
|
'objectType': 'activity',
|
|
'verb': 'post',
|
|
'id': 'fake:post#bridgy-fed-create',
|
|
'actor': 'fake:user',
|
|
'object': note_as1,
|
|
'published': '2022-01-02T03:04:05+00:00',
|
|
}
|
|
self.assertEqual([
|
|
('other:alice:target', post_as1),
|
|
('other:bob:target', post_as1),
|
|
], OtherFake.sent)
|
|
|
|
def test_create_or_update_post_undeletes(self):
|
|
for verb in 'post', 'update':
|
|
with self.subTest(verb=verb):
|
|
id = f'fake:{verb}'
|
|
obj = self.store_object(id=id, source_protocol='fake', deleted=True)
|
|
new_obj_as1 = {
|
|
'objectType': 'note',
|
|
'id': id,
|
|
'content': 'foo',
|
|
}
|
|
_, status = Fake.receive_as1({
|
|
'id': f'{id}#activity',
|
|
'objectType': 'activity',
|
|
'verb': verb,
|
|
'actor': 'fake:user',
|
|
'object': new_obj_as1,
|
|
})
|
|
self.assertEqual(204, status)
|
|
self.assert_object(id,
|
|
our_as1=new_obj_as1,
|
|
users=[self.user.key],
|
|
deleted=False,
|
|
)
|
|
|
|
def test_create_or_update_actor_undeletes(self):
|
|
for verb in 'post', 'update':
|
|
with self.subTest(verb=verb):
|
|
id = f'fake:{verb}'
|
|
obj = self.store_object(id=id, source_protocol='fake', deleted=True)
|
|
new_obj_as1 = {
|
|
'objectType': 'person',
|
|
'id': id,
|
|
'content': 'foo',
|
|
}
|
|
_, status = Fake.receive_as1({
|
|
'id': f'{id}#activity',
|
|
'objectType': 'activity',
|
|
'verb': verb,
|
|
'actor': id,
|
|
'object': new_obj_as1,
|
|
}, authed_as=id)
|
|
self.assertEqual(204, status)
|
|
self.assert_object(id,
|
|
our_as1=new_obj_as1,
|
|
users=[Fake(id=id).key],
|
|
deleted=False,
|
|
)
|
|
|
|
def test_update_post_wrong_actor_error(self):
|
|
post_as1 = {
|
|
'id': 'fake:post',
|
|
'objectType': 'note',
|
|
'author': 'fake:user',
|
|
'content': 'foo',
|
|
}
|
|
self.store_object(id='fake:post', our_as1=post_as1, source_protocol='fake')
|
|
|
|
with self.assertRaises(ErrorButDoNotRetryTask):
|
|
Fake.receive_as1({
|
|
'id': 'fake:update',
|
|
'objectType': 'activity',
|
|
'verb': 'update',
|
|
'actor': 'fake:other',
|
|
'object': post_as1,
|
|
}, authed_as='fake:eve')
|
|
|
|
def test_update_post(self):
|
|
self.make_followers()
|
|
|
|
orig_as1 = {
|
|
'id': 'fake:post',
|
|
'objectType': 'note',
|
|
'author': 'fake:user',
|
|
'content': 'foo',
|
|
}
|
|
self.store_object(id='fake:post', our_as1=orig_as1, source_protocol='fake',
|
|
copies=[Target(uri='other:post', protocol='other')])
|
|
|
|
new_as1 = {
|
|
**orig_as1,
|
|
'content': 'bar',
|
|
}
|
|
update_as1 = {
|
|
'id': 'fake:update',
|
|
'objectType': 'activity',
|
|
'verb': 'update',
|
|
'actor': 'fake:user',
|
|
'object': new_as1,
|
|
}
|
|
self.assertEqual(('OK', 202), Fake.receive_as1(update_as1))
|
|
|
|
self.assert_object('fake:post',
|
|
our_as1=new_as1,
|
|
type='note',
|
|
users=[self.user.key],
|
|
copies=[Target(uri='other:post', protocol='other')],
|
|
deleted=False,
|
|
)
|
|
self.assertIsNone(Object.get_by_id('fake:update'))
|
|
|
|
self.assertEqual([
|
|
('other:alice:target', update_as1),
|
|
('other:bob:target', update_as1),
|
|
], OtherFake.sent)
|
|
|
|
def test_update_post_bare_object(self):
|
|
self.make_followers()
|
|
|
|
post_as1 = {
|
|
'id': 'fake:post',
|
|
'objectType': 'note',
|
|
'author': 'fake:user',
|
|
'content': 'first',
|
|
}
|
|
copy = Target(uri='other:post', protocol='other')
|
|
self.store_object(id='fake:post', our_as1=post_as1,
|
|
source_protocol='fake',
|
|
copies=[copy])
|
|
|
|
post_as1['content'] = 'second'
|
|
_, code = Fake.receive_as1(post_as1, new=False, changed=True)
|
|
self.assertEqual(202, code)
|
|
|
|
self.assert_object('fake:post',
|
|
our_as1={
|
|
**post_as1,
|
|
'updated': '2022-01-02T03:04:05+00:00',
|
|
},
|
|
type='note',
|
|
users=[self.user.key],
|
|
copies=[copy],
|
|
deleted=False,
|
|
)
|
|
|
|
self.assertIsNone(Object.get_by_id(
|
|
'fake:post#bridgy-fed-update-2022-01-02T03:04:05+00:00'))
|
|
self.assertEqual([], Fake.sent)
|
|
|
|
def test_update_post_fetch_object(self):
|
|
post = {
|
|
'id': 'fake:post',
|
|
'objectType': 'note',
|
|
'author': 'fake:user',
|
|
}
|
|
Fake.fetchable['fake:post'] = post
|
|
|
|
self.store_object(id='fake:post', source_protocol='fake')
|
|
|
|
update = {
|
|
'id': 'fake:update',
|
|
'objectType': 'activity',
|
|
'verb': 'update',
|
|
'actor': 'fake:user',
|
|
'object': 'fake:post',
|
|
}
|
|
_, status = Fake.receive_as1(update)
|
|
self.assertEqual(204, status)
|
|
|
|
self.assertEqual(['fake:profile:user', 'fake:post'], Fake.fetched)
|
|
self.assert_object('fake:post',
|
|
our_as1=post,
|
|
type='note',
|
|
)
|
|
self.assertIsNone(Object.get_by_id('fake:update'))
|
|
|
|
def test_update_post_fetch_object_fails(self):
|
|
self.store_object(id='fake:post', source_protocol='fake')
|
|
|
|
update = {
|
|
'id': 'fake:update',
|
|
'objectType': 'activity',
|
|
'verb': 'update',
|
|
'actor': 'fake:user',
|
|
'object': 'fake:post',
|
|
}
|
|
with self.assertRaises(ErrorButDoNotRetryTask):
|
|
Fake.receive_as1(update)
|
|
|
|
self.assertEqual(['fake:profile:user', 'fake:post'], Fake.fetched)
|
|
self.assertIsNone(Object.get_by_id('fake:update'))
|
|
|
|
def test_create_reply(self):
|
|
eve = self.make_user('other:eve', cls=OtherFake, obj_id='other:eve')
|
|
frank = self.make_user('other:frank', cls=OtherFake, obj_id='other:frank')
|
|
Follower.get_or_create(to=self.user, from_=frank)
|
|
|
|
OtherFake.fetchable['other:post'] = {
|
|
'objectType': 'note',
|
|
'author': 'other:eve',
|
|
}
|
|
reply_as1 = {
|
|
'id': 'fake:reply',
|
|
'objectType': 'note',
|
|
'inReplyTo': 'other:post',
|
|
'author': 'fake:user',
|
|
'content': 'foo',
|
|
}
|
|
create_as1 = {
|
|
'id': 'fake:create',
|
|
'objectType': 'activity',
|
|
'verb': 'post',
|
|
'actor': 'fake:user',
|
|
'object': reply_as1,
|
|
}
|
|
self.assertEqual(('OK', 202), Fake.receive_as1(create_as1))
|
|
|
|
self.assert_object(
|
|
'fake:reply',
|
|
our_as1=reply_as1,
|
|
type='note',
|
|
users=[self.user.key],
|
|
copies=[Target(protocol='other', uri='other:o:fa:fake:reply')],
|
|
notify=[eve.key],
|
|
deleted=False,
|
|
)
|
|
self.assertIsNone(Object.get_by_id('fake:create'))
|
|
# not a self reply, shouldn't deliver to follower frank
|
|
self.assertEqual([('other:post:target', create_as1)], OtherFake.sent)
|
|
|
|
def test_create_reply_bare_object(self):
|
|
eve = self.make_user('other:eve', cls=OtherFake, obj_id='other:eve')
|
|
|
|
reply_as1 = {
|
|
'id': 'fake:reply',
|
|
'objectType': 'note',
|
|
'inReplyTo': 'other:post',
|
|
'author': 'fake:user',
|
|
'content': 'foo',
|
|
}
|
|
OtherFake.fetchable['other:post'] = {
|
|
'objectType': 'note',
|
|
'id': 'other:post',
|
|
'author': 'other:eve',
|
|
}
|
|
self.assertEqual(('OK', 202), Fake.receive_as1(reply_as1))
|
|
|
|
self.assert_object(
|
|
'fake:reply',
|
|
our_as1=reply_as1,
|
|
type='note',
|
|
users=[self.user.key],
|
|
notify=[eve.key],
|
|
copies=[Target(protocol='other', uri='other:o:fa:fake:reply')],
|
|
deleted=False,
|
|
)
|
|
|
|
create_as1 = {
|
|
'id': 'fake:reply#bridgy-fed-create',
|
|
'objectType': 'activity',
|
|
'verb': 'post',
|
|
'actor': 'fake:user',
|
|
'object': reply_as1,
|
|
'published': '2022-01-02T03:04:05+00:00',
|
|
}
|
|
self.assertIsNone(Object.get_by_id('fake:reply#bridgy-fed-create'))
|
|
self.assertEqual([('other:post:target', create_as1)], OtherFake.sent)
|
|
|
|
def test_create_reply_to_self_delivers_to_followers(self):
|
|
eve = self.make_user('other:eve', cls=OtherFake, obj_id='other:eve')
|
|
Follower.get_or_create(to=self.user, from_=eve)
|
|
|
|
reply_as1 = {
|
|
'id': 'fake:reply',
|
|
'objectType': 'note',
|
|
'inReplyTo': 'fake:post',
|
|
'author': 'fake:user',
|
|
'content': 'foo',
|
|
}
|
|
self.store_object(id='fake:post', source_protocol='fake',
|
|
copies=[Target(protocol='other', uri='other:post')],
|
|
our_as1={
|
|
'objectType': 'note',
|
|
'id': 'fake:post',
|
|
'author': 'fake:user',
|
|
})
|
|
self.assertEqual(('OK', 202), Fake.receive_as1(reply_as1))
|
|
|
|
self.assert_object(
|
|
'fake:reply',
|
|
our_as1=reply_as1,
|
|
type='note',
|
|
users=[self.user.key],
|
|
copies=[Target(protocol='other', uri='other:o:fa:fake:reply')],
|
|
deleted=False,
|
|
)
|
|
|
|
self.assertIsNone(Object.get_by_id('fake:reply#bridgy-fed-create'))
|
|
self.assertEqual([], Fake.sent)
|
|
|
|
def test_create_reply_to_other_protocol(self):
|
|
eve = self.make_user('fake:eve', cls=Fake, obj_id='fake:eve')
|
|
self.store_object(id='fake:post', source_protocol='fake',
|
|
copies=[Target(protocol='other', uri='other:post')],
|
|
our_as1={
|
|
'objectType': 'note',
|
|
'id': 'fake:post',
|
|
'author': 'fake:eve',
|
|
})
|
|
self.store_object(id='other:post', source_protocol='other',
|
|
our_as1={
|
|
'objectType': 'note',
|
|
'id': 'other:post',
|
|
'author': 'fake:eve',
|
|
})
|
|
|
|
reply_as1 = {
|
|
'id': 'fake:reply',
|
|
'objectType': 'note',
|
|
'inReplyTo': 'fake:post',
|
|
'author': 'fake:user',
|
|
'content': 'foo',
|
|
}
|
|
self.assertEqual(('OK', 202), Fake.receive_as1(reply_as1))
|
|
|
|
copy = Target(protocol='other', uri='other:o:fa:fake:reply')
|
|
reply = self.assert_object('fake:reply', our_as1=reply_as1, type='note',
|
|
users=[self.user.key], copies=[copy], deleted=False)
|
|
self.assertEqual([('other:post:target', {
|
|
'objectType': 'activity',
|
|
'verb': 'post',
|
|
'id': 'fake:reply#bridgy-fed-create',
|
|
'published': '2022-01-02T03:04:05+00:00',
|
|
'actor': 'fake:user',
|
|
'object': reply_as1,
|
|
})], OtherFake.sent)
|
|
|
|
def test_create_reply_with_copy_on_not_enabled_protocol(self):
|
|
self.store_object(id='fake:post', source_protocol='fake',
|
|
copies=[Target(protocol='efake', uri='efake:post')],
|
|
our_as1={
|
|
'objectType': 'note',
|
|
'id': 'fake:post',
|
|
'author': 'fake:alice',
|
|
})
|
|
|
|
_, code = Fake.receive_as1({
|
|
'objectType': 'note',
|
|
'id': 'fake:reply',
|
|
'author': 'fake:user',
|
|
'inReplyTo': 'fake:post',
|
|
'content': 'foo',
|
|
})
|
|
self.assertEqual(204, code)
|
|
self.assertEqual([], ExplicitFake.sent)
|
|
self.assertEqual([], Fake.sent)
|
|
|
|
def test_create_self_reply_to_same_protocol_bridge_if_original_is_bridged(self):
|
|
# use efake because Protocol.targets automatically adds fake and other
|
|
# to to_protocols.
|
|
# TODO: refactor tests to not do fake-to-fake delivery, then remove
|
|
# these special cases
|
|
user = self.make_user('efake:user', cls=ExplicitFake,
|
|
obj_id='efake:user', enabled_protocols=['other'])
|
|
|
|
# eve follows user
|
|
eve = self.make_user('other:eve', cls=OtherFake, obj_id='other:eve')
|
|
Follower.get_or_create(to=user, from_=eve)
|
|
|
|
# user replies to themselves
|
|
self.store_object(id='efake:post', source_protocol='efake',
|
|
copies=[Target(protocol='other', uri='other:post')],
|
|
our_as1={
|
|
'objectType': 'note',
|
|
'id': 'efake:post',
|
|
'author': 'efake:user',
|
|
})
|
|
|
|
reply_as1 = {
|
|
'id': 'efake:reply',
|
|
'objectType': 'note',
|
|
'inReplyTo': 'efake:post',
|
|
'author': 'efake:user',
|
|
'content': 'foo',
|
|
}
|
|
self.assertEqual(('OK', 202), ExplicitFake.receive_as1(reply_as1))
|
|
|
|
copy = Target(protocol='other', uri='other:o:efake:efake:reply')
|
|
reply = self.assert_object('efake:reply',
|
|
type='note',
|
|
source_protocol='efake',
|
|
our_as1=reply_as1,
|
|
users=[user.key],
|
|
copies=[copy],
|
|
deleted=False,
|
|
)
|
|
expected_create = {
|
|
'objectType': 'activity',
|
|
'verb': 'post',
|
|
'id': 'efake:reply#bridgy-fed-create',
|
|
'published': '2022-01-02T03:04:05+00:00',
|
|
'actor': 'efake:user',
|
|
'object': reply_as1,
|
|
}
|
|
|
|
self.assertEqual([('other:eve:target', expected_create),
|
|
('other:post:target', expected_create),
|
|
], OtherFake.sent)
|
|
|
|
def test_update_reply(self):
|
|
eve = self.make_user('other:eve', cls=OtherFake, obj_id='other:eve')
|
|
|
|
OtherFake.fetchable['other:post'] = {
|
|
'objectType': 'note',
|
|
'author': 'other:eve',
|
|
}
|
|
reply_as1 = {
|
|
'id': 'fake:reply',
|
|
'objectType': 'note',
|
|
'inReplyTo': 'other:post',
|
|
'author': 'fake:user',
|
|
'content': 'foo',
|
|
}
|
|
self.store_object(id='fake:reply', our_as1=reply_as1, source_protocol='fake')
|
|
|
|
update_as1 = {
|
|
'id': 'fake:update',
|
|
'objectType': 'activity',
|
|
'verb': 'update',
|
|
'actor': 'fake:user',
|
|
'object': reply_as1,
|
|
}
|
|
self.assertEqual(('OK', 202), Fake.receive_as1(update_as1))
|
|
|
|
self.assert_object('fake:reply',
|
|
our_as1=reply_as1,
|
|
type='note',
|
|
users=[self.user.key],
|
|
notify=[eve.key],
|
|
deleted=False,
|
|
)
|
|
self.assertIsNone(Object.get_by_id('fake:update'))
|
|
self.assertEqual([('other:post:target', update_as1)], OtherFake.sent)
|
|
|
|
def test_repost(self):
|
|
self.make_followers()
|
|
|
|
OtherFake.fetchable['other:post'] = {
|
|
'objectType': 'note',
|
|
'author': 'other:bob',
|
|
}
|
|
repost_as1 = {
|
|
'id': 'fake:repost',
|
|
'objectType': 'activity',
|
|
'verb': 'share',
|
|
'actor': 'fake:user',
|
|
'object': 'other:post',
|
|
}
|
|
self.assertEqual(('OK', 202), Fake.receive_as1(repost_as1))
|
|
|
|
obj = self.assert_object('fake:repost',
|
|
our_as1=repost_as1,
|
|
copies=[Target(protocol='other',
|
|
uri='other:o:fa:fake:repost')],
|
|
type='share',
|
|
users=[self.user.key],
|
|
notify=[self.bob.key],
|
|
)
|
|
self.assertEqual([
|
|
('other:alice:target', obj.as1),
|
|
('other:bob:target', obj.as1),
|
|
('other:post:target', obj.as1),
|
|
], OtherFake.sent)
|
|
|
|
def test_repost_twitter_blocklisted(self):
|
|
"""Reposts of non-fediverse (ie blocklisted) sites aren't yet supported."""
|
|
repost_as1 = {
|
|
'id': 'fake:repost',
|
|
'objectType': 'activity',
|
|
'verb': 'share',
|
|
'actor': 'fake:user',
|
|
'object': 'https://twitter.com/foo',
|
|
}
|
|
_, code = Fake.receive_as1(repost_as1)
|
|
self.assertEqual(204, code)
|
|
|
|
obj = self.assert_object('fake:repost',
|
|
our_as1=repost_as1,
|
|
type='share',
|
|
users=[self.user.key],
|
|
)
|
|
self.assertEqual([], Fake.sent)
|
|
|
|
def test_repost_no_object_error(self):
|
|
with self.assertRaises(ErrorButDoNotRetryTask):
|
|
Fake.receive_as1({
|
|
'id': 'fake:share',
|
|
'objectType': 'activity',
|
|
'verb': 'share',
|
|
'actor': 'fake:user',
|
|
'object': None,
|
|
})
|
|
|
|
def test_like(self):
|
|
OtherFake.fetchable['other:post'] = {
|
|
'objectType': 'note',
|
|
'author': 'other:bob',
|
|
}
|
|
|
|
like_as1 = {
|
|
'id': 'fake:like',
|
|
'objectType': 'activity',
|
|
'verb': 'like',
|
|
'actor': 'fake:user',
|
|
'object': 'other:post',
|
|
}
|
|
self.assertEqual(('OK', 202), Fake.receive_as1(like_as1))
|
|
|
|
like_obj = self.assert_object('fake:like',
|
|
our_as1=like_as1,
|
|
copies=[Target(protocol='other',
|
|
uri='other:o:fa:fake:like')],
|
|
users=[self.user.key],
|
|
notify=[self.bob.key],
|
|
type='like',
|
|
)
|
|
|
|
self.assertEqual([('other:post:target', like_obj.as1)], OtherFake.sent)
|
|
|
|
def test_like_no_object_error(self):
|
|
with self.assertRaises(ErrorButDoNotRetryTask):
|
|
Fake.receive_as1({
|
|
'id': 'fake:like',
|
|
'objectType': 'activity',
|
|
'verb': 'like',
|
|
'actor': 'fake:user',
|
|
'object': None,
|
|
})
|
|
|
|
def test_delete(self):
|
|
self.make_followers()
|
|
|
|
post_as1 = {
|
|
'id': 'fake:post',
|
|
'objectType': 'note',
|
|
'author': 'fake:user',
|
|
}
|
|
self.store_object(id='fake:post', our_as1=post_as1, source_protocol='fake',
|
|
copies=[Target(protocol='other', uri='other:post')])
|
|
|
|
delete_as1 = {
|
|
'id': 'fake:delete',
|
|
'objectType': 'activity',
|
|
'verb': 'delete',
|
|
'actor': 'fake:user',
|
|
'object': 'fake:post',
|
|
}
|
|
self.assertEqual(('OK', 202),
|
|
Fake.receive_as1(delete_as1, authed_as='fake:user'))
|
|
|
|
self.assert_object('fake:post',
|
|
our_as1=post_as1,
|
|
deleted=True,
|
|
source_protocol='fake',
|
|
copies=[Target(protocol='other', uri='other:post')],
|
|
)
|
|
|
|
self.assertIsNone(Object.get_by_id('fake:delete'))
|
|
self.assertEqual('other:alice:target', OtherFake.sent[0][0])
|
|
self.assertEqual('other:bob:target', OtherFake.sent[1][0])
|
|
self.assertEqual('fake:delete', OtherFake.sent[0][1]['id'])
|
|
self.assertEqual('fake:delete', OtherFake.sent[1][1]['id'])
|
|
|
|
def test_delete_doesnt_fetch_author(self):
|
|
self.user.obj_key.delete()
|
|
|
|
delete_as1 = {
|
|
'id': 'fake:delete',
|
|
'objectType': 'activity',
|
|
'verb': 'delete',
|
|
'actor': 'fake:user',
|
|
'object': 'fake:post',
|
|
}
|
|
_, status = Fake.receive_as1(delete_as1, authed_as='fake:user')
|
|
self.assertEqual(204, status)
|
|
|
|
self.assertIsNone(Object.get_by_id('fake:delete'))
|
|
self.assertEqual([], Fake.fetched)
|
|
|
|
def test_delete_no_followers_no_stored_object(self):
|
|
delete_as1 = {
|
|
'id': 'fake:delete',
|
|
'objectType': 'activity',
|
|
'verb': 'delete',
|
|
'actor': 'fake:user',
|
|
'object': 'fake:post',
|
|
}
|
|
_, code = Fake.receive_as1(delete_as1)
|
|
self.assertEqual(204, code)
|
|
|
|
self.assertIsNone(Object.get_by_id('fake:post'))
|
|
self.assertIsNone(Object.get_by_id('fake:delete'))
|
|
self.assertEqual([], Fake.sent)
|
|
|
|
def test_delete_not_authed_as_object_owner(self):
|
|
self.make_followers()
|
|
|
|
self.store_object(id='fake:post', source_protocol='fake', our_as1={
|
|
'objectType': 'note',
|
|
'id': 'fake:post',
|
|
'author': 'fake:user',
|
|
})
|
|
|
|
with self.assertRaises(ErrorButDoNotRetryTask):
|
|
Fake.receive_as1({
|
|
'objectType': 'activity',
|
|
'verb': 'delete',
|
|
'id': 'fake:delete',
|
|
'actor': 'fake:user',
|
|
'object': 'fake:post',
|
|
}, authed_as='fake:eve')
|
|
|
|
self.assertFalse(Object.get_by_id('fake:post').deleted)
|
|
self.assertEqual([], Fake.sent)
|
|
|
|
def test_delete_actor(self):
|
|
self.alice.obj.copies = [Target(protocol='fake', uri='fa:profile:other:alice')]
|
|
self.alice.obj.put()
|
|
|
|
follower = Follower.get_or_create(to=self.user, from_=self.alice)
|
|
followee = Follower.get_or_create(to=self.alice, from_=self.user)
|
|
other = Follower.get_or_create(to=self.user, from_=self.bob)
|
|
self.assertEqual(3, Follower.query().count())
|
|
|
|
_, code = OtherFake.receive_as1({
|
|
'objectType': 'activity',
|
|
'verb': 'delete',
|
|
'id': 'other:delete',
|
|
'actor': 'other:alice',
|
|
'object': 'other:alice',
|
|
})
|
|
self.assertEqual(202, code)
|
|
|
|
self.assertEqual(3, Follower.query().count())
|
|
self.assertEqual('inactive', follower.key.get().status)
|
|
self.assertEqual('inactive', followee.key.get().status)
|
|
self.assertEqual('active', other.key.get().status)
|
|
self.assert_object('other:alice', deleted=True, source_protocol='other',
|
|
ignore=['copies'])
|
|
|
|
@patch.object(Fake, 'send')
|
|
def test_send_error(self, mock_send):
|
|
"""Two targets. First send fails, second succeeds."""
|
|
self.make_followers()
|
|
|
|
post_as1 = {
|
|
'id': 'fake:post',
|
|
'objectType': 'note',
|
|
'content': 'foo',
|
|
}
|
|
create_as1 = {
|
|
'id': 'fake:create',
|
|
'objectType': 'activity',
|
|
'verb': 'post',
|
|
'actor': 'fake:user',
|
|
'object': post_as1,
|
|
}
|
|
|
|
sent = []
|
|
def send(obj, url, from_user=None, orig_obj_id=None):
|
|
self.assertEqual(create_as1, obj.as1)
|
|
if not sent:
|
|
self.assertEqual('other:alice:target', url)
|
|
sent.append('fail')
|
|
raise BadRequest()
|
|
else:
|
|
self.assertEqual('other:bob:target', url)
|
|
sent.append('sent')
|
|
return True
|
|
|
|
mock_send.side_effect = send
|
|
|
|
self.assertEqual(('OK', 202), Fake.receive_as1(create_as1))
|
|
|
|
self.assert_object('fake:post',
|
|
our_as1=post_as1,
|
|
type='note',
|
|
users=[self.user.key],
|
|
deleted=False,
|
|
)
|
|
self.assertIsNone(Object.get_by_id('fake:create'))
|
|
self.assertEqual(['fail', 'sent'], sent)
|
|
|
|
def test_update_profile(self):
|
|
self.user.obj = self.store_object(
|
|
id='fake:profile:user',
|
|
copies = [Target(protocol='other', uri='other:profile:fake:user')])
|
|
self.user.put()
|
|
|
|
self.make_followers()
|
|
|
|
actor = {
|
|
'objectType': 'person',
|
|
'id': 'fake:user',
|
|
'displayName': 'Ms. ☕ Baz',
|
|
'urls': [{'displayName': 'Ms. ☕ Baz', 'value': 'https://user.com/'}],
|
|
'updated': '2022-01-02T03:04:05+00:00',
|
|
}
|
|
id = 'fake:user#update-2022-01-02T03:04:05+00:00'
|
|
update_as1 = {
|
|
'objectType': 'activity',
|
|
'verb': 'update',
|
|
'id': id,
|
|
'actor': actor,
|
|
'object': {**actor, 'id': 'fake:profile:user'},
|
|
}
|
|
Fake.receive_as1(update_as1)
|
|
|
|
# profile object
|
|
self.assert_object('fake:profile:user',
|
|
our_as1=update_as1['object'],
|
|
copies=self.user.obj.copies,
|
|
users=[self.user.key],
|
|
deleted=False,
|
|
)
|
|
|
|
# update activity
|
|
self.assertIsNone(Object.get_by_id(id))
|
|
self.assertEqual([
|
|
('other:alice:target', update_as1),
|
|
('other:bob:target', update_as1),
|
|
], OtherFake.sent)
|
|
|
|
def test_update_profile_bare_object(self):
|
|
profile = {
|
|
'objectType': 'person',
|
|
'id': 'other:alice',
|
|
'displayName': 'Ms. ☕ Baz',
|
|
'summary': 'first',
|
|
}
|
|
self.alice.obj = self.store_object(
|
|
id='other:alice',
|
|
copies = [Target(protocol='fake', uri='fake:profile:other:alice')])
|
|
self.alice.put()
|
|
|
|
Follower.get_or_create(to=self.alice, from_=self.user)
|
|
|
|
# unchanged from what's already in the datastore. we should send update
|
|
# anyway (instead of create) since it's an actor.
|
|
OtherFake.receive_as1(profile)
|
|
|
|
# profile object
|
|
profile['updated'] = '2022-01-02T03:04:05+00:00'
|
|
self.assert_object('other:alice',
|
|
our_as1=profile,
|
|
users=[self.alice.key],
|
|
copies=[Target(protocol='fake',
|
|
uri='fake:profile:other:alice')],
|
|
source_protocol='other',
|
|
deleted=False,
|
|
)
|
|
self.assertEqual([('fake:shared:target', {
|
|
'objectType': 'activity',
|
|
'verb': 'update',
|
|
'id': 'other:alice#bridgy-fed-update-2022-01-02T03:04:05+00:00',
|
|
'actor': profile,
|
|
'object': profile,
|
|
})], Fake.sent)
|
|
|
|
def test_update_profile_use_instead(self):
|
|
user_instead = self.make_user('fake:user-instead', cls=Fake,
|
|
use_instead=self.user.key)
|
|
|
|
profile = {
|
|
'objectType': 'person',
|
|
'id': 'fake:profile:user-instead',
|
|
'foo': 'bar',
|
|
}
|
|
obj = Object(id='fake:profile:user', source_protocol='fake', our_as1=profile)
|
|
Fake.receive(obj)
|
|
|
|
# profile object
|
|
self.assert_object('fake:profile:user',
|
|
our_as1={
|
|
**profile,
|
|
'updated': '2022-01-02T03:04:05+00:00',
|
|
},
|
|
source_protocol='fake',
|
|
users=[self.user.key],
|
|
deleted=False,
|
|
)
|
|
self.assertIsNone(Object.get_by_id(
|
|
'fake:profile:user#bridgy-fed-update-2022-01-02T03:04:05+00:00'))
|
|
|
|
def test_mention_object(self, *mocks):
|
|
self.alice.obj.our_as1 = {'id': 'other:alice', 'objectType': 'person'}
|
|
self.alice.obj.put()
|
|
self.bob.obj.our_as1 = {'id': 'other:bob', 'objectType': 'person'}
|
|
self.bob.obj.put()
|
|
|
|
mention_as1 = {
|
|
'objectType': 'note',
|
|
'id': 'fake:mention',
|
|
'author': 'fake:user',
|
|
'content': 'something',
|
|
'tags': [{
|
|
'objectType': 'mention',
|
|
'url': 'other:alice',
|
|
}, {
|
|
'objectType': 'mention',
|
|
'url': 'other:bob',
|
|
}],
|
|
}
|
|
self.assertEqual(('OK', 202), Fake.receive_as1(mention_as1))
|
|
|
|
self.assert_object('fake:mention',
|
|
our_as1=mention_as1,
|
|
type='note',
|
|
users=[self.user.key],
|
|
notify=[self.alice.key, self.bob.key],
|
|
copies=[Target(protocol='other',
|
|
uri='other:o:fa:fake:mention')],
|
|
deleted=False,
|
|
)
|
|
|
|
self.assertIsNone(Object.get_by_id('fake:post#bridgy-fed-create'))
|
|
self.assertEqual('other:alice:target', OtherFake.sent[0][0])
|
|
self.assertEqual('other:bob:target', OtherFake.sent[1][0])
|
|
self.assertEqual('fake:mention#bridgy-fed-create', OtherFake.sent[0][1]['id'])
|
|
self.assertEqual('fake:mention#bridgy-fed-create', OtherFake.sent[1][1]['id'])
|
|
|
|
def test_follow(self):
|
|
self._test_follow()
|
|
|
|
def test_follow_existing_inactive(self):
|
|
follower = Follower.get_or_create(to=self.alice, from_=self.user)
|
|
self._test_follow()
|
|
|
|
def test_follow_actor_object_composite_objects(self):
|
|
self._test_follow(actor={'id': 'fake:user', 'objectType': 'person'},
|
|
object={'id': 'other:alice', 'objectType': 'person'})
|
|
|
|
def _test_follow(self, **extra):
|
|
OtherFake.fetchable['other:alice'] = {}
|
|
|
|
follow_as1 = {
|
|
'id': 'fake:follow',
|
|
'objectType': 'activity',
|
|
'verb': 'follow',
|
|
'actor': 'fake:user',
|
|
'object': 'other:alice',
|
|
**extra,
|
|
}
|
|
self.assertEqual(('OK', 202), Fake.receive_as1(follow_as1))
|
|
|
|
follow_obj = self.assert_object(
|
|
'fake:follow',
|
|
our_as1=follow_as1,
|
|
copies=[Target(protocol='other', uri='other:o:fa:fake:follow')],
|
|
users=[self.user.key],
|
|
notify=[self.alice.key],
|
|
)
|
|
|
|
accept_id = 'other:alice/followers#accept-fake:follow'
|
|
accept_as1 = {
|
|
'id': accept_id,
|
|
'objectType': 'activity',
|
|
'verb': 'accept',
|
|
'actor': 'other:alice',
|
|
'object': follow_as1,
|
|
}
|
|
self.assertEqual([('other:alice:target', follow_obj.as1)], OtherFake.sent)
|
|
self.assertEqual([('fake:user:target', accept_as1)], Fake.sent)
|
|
|
|
self.assert_entities_equal(
|
|
Follower(to=self.alice.key, from_=self.user.key, status='active',
|
|
follow=follow_obj.key),
|
|
Follower.query().fetch(),
|
|
ignore=['created', 'updated'],
|
|
)
|
|
|
|
def test_follow_protocol_that_doesnt_support_accept(self, **extra):
|
|
OtherFake.fetchable['other:eve'] = {}
|
|
|
|
follow_as1 = {
|
|
'id': 'other:follow',
|
|
'objectType': 'activity',
|
|
'verb': 'follow',
|
|
'actor': 'other:eve',
|
|
'object': 'fake:user',
|
|
}
|
|
self.assertEqual(('OK', 202), OtherFake.receive_as1(follow_as1))
|
|
|
|
other = OtherFake.get_by_id('other:eve')
|
|
self.assert_object(
|
|
'other:follow',
|
|
source_protocol='other',
|
|
our_as1=follow_as1,
|
|
copies=[Target(protocol='fake', uri='fake:o:other:other:follow')],
|
|
users=[OtherFake(id='other:eve').key],
|
|
notify=[self.user.key],
|
|
)
|
|
|
|
self.assertEqual(0, Object.query(Object.type == 'accept').count())
|
|
self.assertEqual([], OtherFake.sent)
|
|
|
|
def test_follow_no_actor(self):
|
|
with self.assertRaises(ErrorButDoNotRetryTask):
|
|
Fake.receive_as1({
|
|
'id': 'fake:follow',
|
|
'objectType': 'activity',
|
|
'verb': 'follow',
|
|
'object': 'fake:user',
|
|
})
|
|
|
|
self.assertEqual([], Follower.query().fetch())
|
|
self.assertEqual([], Fake.sent)
|
|
|
|
def test_follow_no_object(self):
|
|
with self.assertRaises(ErrorButDoNotRetryTask):
|
|
Fake.receive_as1({
|
|
'id': 'other:follow',
|
|
'objectType': 'activity',
|
|
'verb': 'follow',
|
|
'actor': 'other:alice',
|
|
})
|
|
|
|
self.assertEqual([], Follower.query().fetch())
|
|
self.assertEqual([], Fake.sent)
|
|
|
|
def test_follow_object_unknown_protocol(self):
|
|
with self.assertRaises(ErrorButDoNotRetryTask):
|
|
Fake.receive_as1({
|
|
'id': 'other:follow',
|
|
'objectType': 'activity',
|
|
'verb': 'follow',
|
|
'actor': 'other:alice',
|
|
'object': 'unknown:bob',
|
|
})
|
|
|
|
self.assertEqual([], Follower.query().fetch())
|
|
self.assertEqual([], Fake.sent)
|
|
|
|
def test_accept_noop(self, **extra):
|
|
eve = self.make_user('other:eve', cls=OtherFake)
|
|
accept_as1 = {
|
|
'id': 'other:accept',
|
|
'objectType': 'activity',
|
|
'verb': 'accept',
|
|
'actor': 'other:eve',
|
|
'object': 'fake:follow'
|
|
}
|
|
|
|
with self.assertRaises(NoContent):
|
|
_, status = OtherFake.receive_as1(accept_as1)
|
|
|
|
self.assertEqual([], Fake.sent)
|
|
self.assertEqual([], OtherFake.sent)
|
|
|
|
@patch('dms.maybe_send')
|
|
def test_follow_accept(self, _, **extra):
|
|
self.user.enable_protocol(ExplicitFake)
|
|
ExplicitFake.fetchable['efake:follow'] = {'id': 'efake:follow'}
|
|
accept_as1 = {
|
|
'id': 'fake:accept',
|
|
'objectType': 'activity',
|
|
'verb': 'accept',
|
|
'actor': 'fake:user',
|
|
'object': 'efake:follow'
|
|
}
|
|
|
|
self.assertEqual(('OK', 202), Fake.receive_as1(accept_as1))
|
|
self.assertEqual([('efake:follow:target', accept_as1)], ExplicitFake.sent)
|
|
|
|
def test_stop_following(self):
|
|
self.user.obj.our_as1 = {'id': 'fake:user'}
|
|
self.user.obj.put()
|
|
|
|
follower = Follower.get_or_create(to=self.user, from_=self.alice)
|
|
|
|
stop_as1 = {
|
|
'id': 'other:stop-following',
|
|
'objectType': 'activity',
|
|
'verb': 'stop-following',
|
|
'actor': 'other:alice',
|
|
'object': 'fake:user',
|
|
}
|
|
_, code = OtherFake.receive_as1(stop_as1)
|
|
self.assertEqual(202, code)
|
|
|
|
self.assertEqual('inactive', follower.key.get().status)
|
|
self.assertEqual([('fake:user:target', stop_as1)], Fake.sent)
|
|
|
|
def test_stop_following_doesnt_exist(self):
|
|
self.user.obj.our_as1 = {'id': 'fake:user'}
|
|
self.user.obj.put()
|
|
|
|
stop_following_as1 = {
|
|
'id': 'other:stop-following',
|
|
'objectType': 'activity',
|
|
'verb': 'stop-following',
|
|
'actor': 'other:alice',
|
|
'object': 'fake:user',
|
|
}
|
|
self.assertEqual(('OK', 202), OtherFake.receive_as1(stop_following_as1))
|
|
|
|
self.assertEqual(0, Follower.query().count())
|
|
self.assertEqual([('fake:user:target', stop_following_as1)], Fake.sent)
|
|
|
|
def test_stop_following_inactive(self):
|
|
self.user.obj.our_as1 = {'id': 'fake:user'}
|
|
self.user.obj.put()
|
|
|
|
follower = Follower.get_or_create(to=self.user, from_=self.alice,
|
|
status='inactive')
|
|
|
|
stop_following_as1 = {
|
|
'id': 'other:stop-following',
|
|
'objectType': 'activity',
|
|
'verb': 'stop-following',
|
|
'actor': 'other:alice',
|
|
'object': 'fake:user',
|
|
}
|
|
self.assertEqual(('OK', 202), OtherFake.receive_as1(stop_following_as1))
|
|
|
|
self.assertEqual('inactive', follower.key.get().status)
|
|
self.assertEqual([('fake:user:target', stop_following_as1)], Fake.sent)
|
|
|
|
def test_block(self):
|
|
self.bob.obj.our_as1 = {'id': 'other:bob'}
|
|
self.bob.obj.put()
|
|
|
|
block_as1 = {
|
|
'id': 'fake:block',
|
|
'objectType': 'activity',
|
|
'verb': 'block',
|
|
'actor': 'fake:user',
|
|
'object': 'other:bob',
|
|
}
|
|
self.assertEqual(('OK', 202), Fake.receive_as1(block_as1))
|
|
self.assertEqual([('other:bob:target', block_as1)], OtherFake.sent)
|
|
|
|
def test_undo_block(self):
|
|
eve = self.make_user(id='other:eve', cls=OtherFake,
|
|
obj_as1={'id': 'other:eve'})
|
|
self.make_followers()
|
|
|
|
block = {
|
|
'objectType': 'activity',
|
|
'verb': 'block',
|
|
'id': 'fake:block',
|
|
'actor': 'fake:user',
|
|
'object': 'other:eve',
|
|
}
|
|
self.store_object(id='fake:block', our_as1=block, source_protocol='fake')
|
|
|
|
undo_as1 = {
|
|
'objectType': 'activity',
|
|
'verb': 'undo',
|
|
'id': 'fake:undo',
|
|
'actor': 'fake:user',
|
|
'object': block,
|
|
}
|
|
self.assertEqual(('OK', 202), Fake.receive_as1(undo_as1))
|
|
self.assertEqual([('other:eve:target', undo_as1)], OtherFake.sent)
|
|
|
|
def test_undo_not_authed_as_object_owner(self):
|
|
self.store_object(id='fake:repost', source_protocol='fake', our_as1={
|
|
'objectType': 'activity',
|
|
'verb': 'share',
|
|
'id': 'fake:repost',
|
|
'actor': 'fake:user',
|
|
'object': 'fake:post',
|
|
})
|
|
|
|
with self.assertRaises(ErrorButDoNotRetryTask):
|
|
Fake.receive_as1({
|
|
'objectType': 'activity',
|
|
'verb': 'undo',
|
|
'id': 'fake:undo',
|
|
'actor': 'fake:user',
|
|
'object': 'fake:repost',
|
|
}, authed_as='fake:eve')
|
|
|
|
self.assertFalse(Object.get_by_id('fake:repost').deleted)
|
|
self.assertEqual([], Fake.sent)
|
|
|
|
@skip
|
|
def test_from_bridgy_fed_domain_fails(self):
|
|
with self.assertRaises(BadRequest):
|
|
Fake.receive_as1({
|
|
'id': 'https://fed.brid.gy/r/foo',
|
|
})
|
|
|
|
self.assertIsNone(Object.get_by_id('https://fed.brid.gy/r/foo'))
|
|
|
|
with self.assertRaises(BadRequest):
|
|
Fake.receive_as1({
|
|
'id': 'fake:foo',
|
|
'actor': 'https://ap.brid.gy/user.com',
|
|
})
|
|
|
|
self.assertIsNone(Object.get_by_id('foo'))
|
|
self.assertIsNone(Object.get_by_id('https://ap.brid.gy/user.com'))
|
|
|
|
def test_skip_same_protocol(self):
|
|
self.make_user('other:carol', cls=OtherFake, obj_id='other:carol')
|
|
self.make_user('other:dan', cls=OtherFake, obj_id='other:dan')
|
|
|
|
OtherFake.fetchable = {
|
|
'other:carol': {},
|
|
}
|
|
|
|
follow_as1 = {
|
|
'id': 'other:follow',
|
|
'objectType': 'activity',
|
|
'verb': 'follow',
|
|
'actor': 'other:carol',
|
|
'object': ['other:dan', 'fake:user'],
|
|
}
|
|
|
|
self.assertEqual(('OK', 202), OtherFake.receive_as1(follow_as1))
|
|
|
|
self.assertEqual(1, len(Fake.sent))
|
|
self.assertEqual('other:follow', Fake.sent[0][1]['id'])
|
|
|
|
followers = Follower.query().fetch()
|
|
self.assertEqual(1, len(followers))
|
|
self.assertEqual(self.user.key, followers[0].to)
|
|
|
|
def test_skip_bridged_user(self):
|
|
"""If the actor isn't from the source protocol, skip the activity.
|
|
|
|
(It's probably from a bridged user, and we only want to handle source
|
|
activities, not bridged activities.)
|
|
"""
|
|
self.user.copies = [Target(uri='other:user', protocol='other')]
|
|
self.user.put()
|
|
|
|
with self.assertRaises(NoContent):
|
|
OtherFake.receive_as1({
|
|
'id': 'other:follow',
|
|
'objectType': 'activity',
|
|
'verb': 'follow',
|
|
'actor': 'fake:user',
|
|
'object': 'other:alice',
|
|
})
|
|
self.assertEqual([], OtherFake.sent)
|
|
self.assertEqual([], Fake.sent)
|
|
self.assertIsNone(Object.get_by_id('other:follow'))
|
|
|
|
@patch('requests.post')
|
|
@patch('requests.get')
|
|
def test_skip_web_same_domain(self, mock_get, mock_post):
|
|
self.make_user('user.com', cls=Web)
|
|
mock_get.side_effect = [
|
|
ACTOR_HTML_RESP,
|
|
NOTE_HTML_RESP,
|
|
NOTE_HTML_RESP,
|
|
NOTE_HTML_RESP,
|
|
NOTE_HTML_RESP,
|
|
]
|
|
|
|
follow_as1 = {
|
|
'id': 'http://user.com/follow',
|
|
'objectType': 'activity',
|
|
'verb': 'follow',
|
|
'actor': 'http://user.com/',
|
|
'object': ['http://user.com/bob', 'http://user.com/eve'],
|
|
}
|
|
|
|
_, code = Web.receive(Object(our_as1=follow_as1), authed_as='user.com')
|
|
self.assertEqual(204, code)
|
|
|
|
mock_post.assert_not_called()
|
|
self.assertEqual(0, Follower.query().count())
|
|
|
|
def test_activity_id_blocklisted(self):
|
|
with self.assertRaises(ErrorButDoNotRetryTask):
|
|
Fake.receive_as1({
|
|
'objectType': 'activity',
|
|
'verb': 'delete',
|
|
'id': 'fake:blocklisted:delete',
|
|
'actor': 'fake:user',
|
|
'object': 'fake:foo',
|
|
})
|
|
|
|
def test_resolve_ids_follow(self):
|
|
follow = {
|
|
'id': 'fake:follow',
|
|
'objectType': 'activity',
|
|
'verb': 'follow',
|
|
'actor': 'fake:user',
|
|
'object': 'fake:alice',
|
|
}
|
|
|
|
# no matching copy users
|
|
obj = Object(id='fake:follow', our_as1=follow, source_protocol='fake')
|
|
_, code = Fake.receive(obj, authed_as='fake:user')
|
|
self.assertEqual(204, code)
|
|
self.assert_equals(follow, obj.our_as1)
|
|
|
|
# matching copy user
|
|
self.alice.copies = [Target(uri='fake:alice', protocol='fake')]
|
|
self.alice.put()
|
|
|
|
models.get_original_user_key.cache_clear()
|
|
models.get_original_object_key.cache_clear()
|
|
memcache.memcache.client_pool.clear()
|
|
memcache.pickle_memcache.client_pool.clear()
|
|
|
|
obj.new = True
|
|
Fake.fetchable = {
|
|
'fake:alice': {},
|
|
}
|
|
|
|
self.assertEqual(('OK', 202), Fake.receive(obj, authed_as='fake:user'))
|
|
self.assert_equals({
|
|
**follow,
|
|
'actor': 'fake:user',
|
|
'object': 'other:alice',
|
|
}, Object.get_by_id('fake:follow').our_as1)
|
|
|
|
def test_resolve_ids_share(self):
|
|
share = {
|
|
'objectType': 'activity',
|
|
'actor': 'fake:user',
|
|
'verb': 'share',
|
|
'object': 'fake:post',
|
|
}
|
|
|
|
# no matching copy object
|
|
obj = Object(id='fake:share', our_as1=share, source_protocol='fake')
|
|
_, code = Fake.receive(obj, authed_as='fake:user')
|
|
self.assertEqual(204, code)
|
|
self.assert_equals(share, obj.our_as1)
|
|
|
|
# matching copy object
|
|
self.store_object(id='other:post',
|
|
copies=[Target(uri='fake:post', protocol='fake')])
|
|
|
|
models.get_original_user_key.cache_clear()
|
|
models.get_original_object_key.cache_clear()
|
|
memcache.memcache.client_pool.clear()
|
|
memcache.pickle_memcache.client_pool.clear()
|
|
obj.new = True
|
|
|
|
_, code = Fake.receive(obj, authed_as='fake:user')
|
|
self.assertEqual(204, code)
|
|
|
|
self.assert_equals({
|
|
'id': 'fake:share',
|
|
'objectType': 'activity',
|
|
'actor': 'fake:user',
|
|
'verb': 'share',
|
|
'object': 'other:post',
|
|
}, obj.our_as1)
|
|
|
|
def test_resolve_ids_reply_mentions(self):
|
|
reply = {
|
|
'id': 'fake:reply',
|
|
'author': 'fake:user',
|
|
'objectType': 'note',
|
|
'content': 'foo',
|
|
'inReplyTo': [
|
|
'fake:unknown-post',
|
|
'fake:post',
|
|
],
|
|
'tags': [{
|
|
'objectType': 'mention',
|
|
'url': 'fake:alice',
|
|
}, {
|
|
'objectType': 'mention',
|
|
'url': 'fake:bob',
|
|
}],
|
|
}
|
|
|
|
# no matching copies
|
|
_, code = Fake.receive_as1(reply)
|
|
self.assertEqual(204, code)
|
|
self.assert_equals(reply, Object.get_by_id('fake:reply').our_as1)
|
|
|
|
# matching copies
|
|
self.alice.copies=[Target(uri='fake:alice', protocol='fake')]
|
|
self.alice.put()
|
|
self.alice.obj = Object(id='other:alice', our_as1={'a': 'b'})
|
|
self.alice.obj.our_as1 = {'a': 'b'}
|
|
|
|
self.bob.copies=[Target(uri='fake:bob', protocol='fake')]
|
|
self.bob.put()
|
|
self.bob.obj = Object(id='other:bob', our_as1={'x': 'y'})
|
|
self.bob.obj.our_as1 = {'a': 'b'}
|
|
self.bob.obj.put()
|
|
|
|
models.get_original_user_key.cache_clear()
|
|
models.get_original_object_key.cache_clear()
|
|
memcache.pickle_memcache.client_pool.clear()
|
|
|
|
self.assertEqual(('OK', 202), Fake.receive_as1(reply, new=True))
|
|
self.assertEqual({
|
|
'id': 'fake:reply',
|
|
'objectType': 'note',
|
|
'author': 'fake:user',
|
|
'content': 'foo',
|
|
'inReplyTo': [
|
|
'fake:unknown-post',
|
|
'fake:post',
|
|
],
|
|
'tags': [{
|
|
'objectType': 'mention',
|
|
'url': 'other:alice',
|
|
}, {
|
|
'objectType': 'mention',
|
|
'url': 'other:bob',
|
|
}],
|
|
}, Object.get_by_id('fake:reply').our_as1)
|
|
|
|
def test_follow_and_block_protocol_user_sets_enabled_protocols(self):
|
|
# bot user
|
|
self.make_user('fa.brid.gy', cls=Web)
|
|
|
|
follow = {
|
|
'objectType': 'activity',
|
|
'verb': 'follow',
|
|
'id': 'efake:follow',
|
|
'actor': 'efake:user',
|
|
'object': 'fa.brid.gy',
|
|
}
|
|
block = {
|
|
'objectType': 'activity',
|
|
'verb': 'block',
|
|
'id': 'efake:block',
|
|
'actor': 'efake:user',
|
|
'object': 'fa.brid.gy',
|
|
}
|
|
|
|
user = self.make_user('efake:user', cls=ExplicitFake)
|
|
self.assertFalse(user.is_enabled(Fake))
|
|
ExplicitFake.fetchable = {'efake:user': {'profile': 'info'}}
|
|
|
|
# fake protocol isn't enabled yet, block should be a noop
|
|
self.assertEqual(('OK', 200), ExplicitFake.receive_as1(block))
|
|
user = user.key.get()
|
|
self.assertEqual([], user.enabled_protocols)
|
|
self.assertEqual([], Fake.created_for)
|
|
|
|
# follow should add to enabled_protocols
|
|
_, code = ExplicitFake.receive_as1(follow)
|
|
self.assertEqual(204, code)
|
|
user = user.key.get()
|
|
self.assertEqual({
|
|
'id': 'efake:user',
|
|
'profile': 'info',
|
|
}, user.obj.as1)
|
|
|
|
self.assertEqual(['fake'], user.enabled_protocols)
|
|
self.assertEqual(['efake:user'], Fake.created_for)
|
|
self.assertTrue(user.is_enabled(Fake))
|
|
|
|
dm_id = 'https://fa.brid.gy/#welcome-dm-efake:user-2022-01-02T03:04:05+00:00-create'
|
|
follow_back_id = 'https://fa.brid.gy/#follow-back-efake:user-2022-01-02T03:04:05+00:00'
|
|
|
|
self.assert_equals([
|
|
# fa.brid.gy follows back
|
|
('efake:user:target', {
|
|
'objectType': 'activity',
|
|
'verb': 'follow',
|
|
'id': 'https://fa.brid.gy/#follow-back-efake:user-2022-01-02T03:04:05+00:00',
|
|
'actor': 'fa.brid.gy',
|
|
'object': 'efake:user',
|
|
}),
|
|
# accept follow
|
|
('efake:user:target', {
|
|
'objectType': 'activity',
|
|
'verb': 'accept',
|
|
'id': 'fa.brid.gy/followers#accept-efake:follow',
|
|
'actor': 'fa.brid.gy',
|
|
'object': {
|
|
**follow,
|
|
'actor': {'id': 'efake:user', 'profile': 'info'},
|
|
},
|
|
}),
|
|
], ExplicitFake.sent[1:])
|
|
|
|
ExplicitFake.sent = ExplicitFake.sent[:1]
|
|
test_dms.DmsTest().assert_sent(Fake, user, 'welcome', 'Welcome to Bridgy Fed! Your account will soon be bridged to fake-phrase at <a class="h-card u-author" rel="me" href="web:fake:efake:user" title="fake:handle:efake:handle:user">fake:handle:efake:handle:user</a>. <a href="https://fed.brid.gy/docs">See the docs</a> and <a href="https://fed.brid.gy/efake/efake:handle:user">your user page</a> for more information. To disable this and delete your bridged profile, block this account.')
|
|
|
|
# another follow should be a noop
|
|
follow['id'] += '2'
|
|
Fake.created_for = []
|
|
_, code = ExplicitFake.receive_as1(follow)
|
|
self.assertEqual(204, code)
|
|
user = user.key.get()
|
|
self.assertEqual(['fake'], user.enabled_protocols)
|
|
self.assertTrue(user.is_enabled(Fake))
|
|
self.assertEqual(['efake:user'], Fake.created_for)
|
|
|
|
# block should remove from enabled_protocols
|
|
Follower.get_or_create(to=user, from_=self.user)
|
|
block['id'] += '2'
|
|
self.assertEqual(('OK', 200), ExplicitFake.receive_as1(block))
|
|
user = user.key.get()
|
|
self.assertEqual([], user.enabled_protocols)
|
|
self.assertFalse(user.is_enabled(Fake))
|
|
|
|
# ...and delete copy actor
|
|
id = 'efake:user#bridgy-fed-delete-user-fake-2022-01-02T03:04:05+00:00'
|
|
delete_efake = {
|
|
'objectType': 'activity',
|
|
'verb': 'delete',
|
|
'id': id,
|
|
'actor': 'efake:user',
|
|
'object': 'efake:user',
|
|
}
|
|
self.assertEqual([('fake:shared:target', delete_efake)], Fake.sent)
|
|
self.assertIsNone(Object.get_by_id(id))
|
|
|
|
def test_follow_bot_user_refreshes_profile(self):
|
|
# bot user
|
|
self.make_user('fa.brid.gy', cls=Web)
|
|
|
|
# store profile that's opted out
|
|
user = self.make_user('efake:user', cls=ExplicitFake, obj_as1={
|
|
'id': 'efake:user',
|
|
'summary': '#nobridge',
|
|
})
|
|
self.assertFalse(user.is_enabled(Fake))
|
|
|
|
# updated profile isn't opted out
|
|
ExplicitFake.fetchable = {'efake:user': {
|
|
'id': 'efake:user',
|
|
'summary': 'never mind',
|
|
}}
|
|
|
|
# follow should refresh profile
|
|
_, code = ExplicitFake.receive_as1({
|
|
'objectType': 'activity',
|
|
'verb': 'follow',
|
|
'id': 'efake:follow',
|
|
'actor': 'efake:user',
|
|
'object': 'fa.brid.gy',
|
|
})
|
|
self.assertEqual(204, code)
|
|
|
|
user = user.key.get()
|
|
self.assertTrue(user.is_enabled(Fake))
|
|
self.assertEqual(['efake:user'], ExplicitFake.fetched)
|
|
|
|
def test_follow_bot_user_copy_id_refreshes_profile(self):
|
|
# bot user
|
|
self.make_user('fa.brid.gy', cls=Web,
|
|
copies=[Target(uri='efake:bot', protocol='efake')])
|
|
|
|
# profile that's opted out
|
|
user = self.make_user('efake:user', cls=ExplicitFake, obj_as1={
|
|
'id': 'efake:user',
|
|
'summary': '#nobridge',
|
|
})
|
|
self.assertFalse(user.is_enabled(Fake))
|
|
|
|
# updated profile isn't opted out
|
|
ExplicitFake.fetchable = {'efake:user': {
|
|
'id': 'efake:user',
|
|
'summary': 'never mind',
|
|
}}
|
|
|
|
# follow should refresh profile
|
|
_, code = ExplicitFake.receive_as1({
|
|
'objectType': 'activity',
|
|
'verb': 'follow',
|
|
'id': 'efake:follow',
|
|
'actor': 'efake:user',
|
|
'object': 'efake:bot',
|
|
})
|
|
self.assertEqual(204, code)
|
|
|
|
user = user.key.get()
|
|
self.assertTrue(user.is_enabled(Fake))
|
|
self.assertEqual(['efake:user'], ExplicitFake.fetched)
|
|
|
|
def test_follow_bot_user_overrides_nobot(self):
|
|
# bot user
|
|
self.make_user('fa.brid.gy', cls=Web,
|
|
copies=[Target(uri='efake:bot', protocol='efake')])
|
|
|
|
# profile that's opted out
|
|
actor = {
|
|
'id': 'efake:user',
|
|
'summary': '#nobot',
|
|
}
|
|
user = self.make_user('efake:user', cls=ExplicitFake, obj_as1=actor)
|
|
self.assertFalse(user.is_enabled(Fake))
|
|
ExplicitFake.fetchable = {'efake:user': actor}
|
|
|
|
# follow should override #nobot
|
|
_, code = ExplicitFake.receive_as1({
|
|
'objectType': 'activity',
|
|
'verb': 'follow',
|
|
'id': 'efake:follow',
|
|
'actor': 'efake:user',
|
|
'object': 'efake:bot',
|
|
})
|
|
self.assertEqual(204, code)
|
|
|
|
user = user.key.get()
|
|
self.assertIsNone(user.status)
|
|
self.assertTrue(user.is_enabled(Fake))
|
|
self.assertEqual(['efake:user'], ExplicitFake.fetched)
|
|
|
|
@patch.object(ExplicitFake, 'REQUIRES_NAME', new=True)
|
|
def test_follow_bot_user_spam_filter_doesnt_enable(self):
|
|
self.make_user('fa.brid.gy', cls=Web,
|
|
copies=[Target(uri='efake:bot', protocol='efake')])
|
|
|
|
user = self.make_user('efake:user', cls=ExplicitFake)
|
|
ExplicitFake.fetchable = {'efake:user': {'id': 'efake:user'}}
|
|
|
|
follow_as1 = {
|
|
'objectType': 'activity',
|
|
'verb': 'follow',
|
|
'id': 'efake:follow',
|
|
'actor': 'efake:user',
|
|
'object': 'efake:bot',
|
|
}
|
|
with self.assertRaises(ErrorButDoNotRetryTask):
|
|
_, code = ExplicitFake.receive_as1(follow_as1)
|
|
|
|
user = user.key.get()
|
|
self.assertEqual('requires-name', user.status)
|
|
self.assertFalse(user.is_enabled(Fake))
|
|
self.assertEqual(['efake:user'], ExplicitFake.fetched)
|
|
|
|
self.assertEqual(2, len(ExplicitFake.sent))
|
|
test_dms.DmsTest().assert_sent(
|
|
Fake, user, 'requires-name',
|
|
"Hi! Your account isn't eligible for bridging yet because your account's name and username are the same.",
|
|
strict=False)
|
|
self.assertEqual(('efake:user:target', {
|
|
'objectType': 'activity',
|
|
'verb': 'reject',
|
|
'id': 'fa.brid.gy/followers#reject-efake:follow',
|
|
'actor': 'fa.brid.gy',
|
|
'object': {
|
|
**follow_as1,
|
|
'actor': {'id': 'efake:user'},
|
|
'object': 'fa.brid.gy',
|
|
},
|
|
}), ExplicitFake.sent[1])
|
|
|
|
|
|
@patch.object(Fake, 'owns_handle', return_value=False)
|
|
@patch.object(ExplicitFake, 'owns_handle', return_value=True)
|
|
def test_follow_bot_user_unsupported_handle(self, _, __):
|
|
self.make_user('fa.brid.gy', cls=Web,
|
|
copies=[Target(uri='efake:bot', protocol='efake')])
|
|
|
|
user = self.make_user('efake:user-nope', cls=ExplicitFake)
|
|
ExplicitFake.fetchable = {'efake:user-nope': {'id': 'efake:user-nope'}}
|
|
|
|
follow_as1 = {
|
|
'objectType': 'activity',
|
|
'verb': 'follow',
|
|
'id': 'efake:follow',
|
|
'actor': 'efake:user-nope',
|
|
'object': 'efake:bot',
|
|
}
|
|
with self.assertRaises(ErrorButDoNotRetryTask):
|
|
_, code = ExplicitFake.receive_as1(follow_as1)
|
|
|
|
user = user.key.get()
|
|
self.assertFalse(user.is_enabled(Fake))
|
|
|
|
self.assertEqual(2, len(ExplicitFake.sent))
|
|
test_dms.DmsTest().assert_sent(
|
|
Fake, user, 'unsupported-handle-fa',
|
|
"Hi! Your account isn't eligible for bridging yet because your handle efake:handle:user-nope translated to fake-phrase is fake:handle:efake:handle:user-nope, which isn't supported there.",
|
|
strict=False)
|
|
self.assertEqual(('efake:user-nope:target', {
|
|
'objectType': 'activity',
|
|
'verb': 'reject',
|
|
'id': 'fa.brid.gy/followers#reject-efake:follow',
|
|
'actor': 'fa.brid.gy',
|
|
'object': {
|
|
**follow_as1,
|
|
'actor': {'id': 'efake:user-nope'},
|
|
'object': 'fa.brid.gy',
|
|
},
|
|
}), ExplicitFake.sent[1])
|
|
|
|
def test_block_then_follow_protocol_user_recreates_copy(self):
|
|
# bot user
|
|
self.make_user('fa.brid.gy', cls=Web)
|
|
|
|
follow = {
|
|
'objectType': 'activity',
|
|
'verb': 'follow',
|
|
'id': 'efake:follow',
|
|
'actor': 'efake:user',
|
|
'object': 'fa.brid.gy',
|
|
}
|
|
block = {
|
|
'objectType': 'activity',
|
|
'verb': 'block',
|
|
'id': 'efake:block',
|
|
'actor': 'efake:user',
|
|
'object': 'fa.brid.gy',
|
|
}
|
|
|
|
copy = Target(uri='fake:user', protocol='fake')
|
|
user = self.make_user('efake:user', cls=ExplicitFake,
|
|
enabled_protocols=['fake'], copies=[copy])
|
|
self.assertTrue(user.is_enabled(Fake))
|
|
self.assertEqual([copy], user.copies)
|
|
|
|
self.assertEqual(('OK', 200), ExplicitFake.receive_as1(block))
|
|
user = user.key.get()
|
|
self.assertFalse(user.is_enabled(Fake))
|
|
self.assertEqual([copy], user.copies)
|
|
|
|
# fake protocol isn't enabled yet, block should be a noop
|
|
ExplicitFake.fetchable = {'efake:user': {'profile': 'info'}}
|
|
_, code = ExplicitFake.receive_as1(follow)
|
|
self.assertEqual(204, code)
|
|
user = user.key.get()
|
|
self.assertEqual(['fake'], user.enabled_protocols)
|
|
self.assertEqual(['efake:user'], Fake.created_for)
|
|
|
|
def test_too_old(self):
|
|
Follower.get_or_create(to=self.user, from_=self.alice)
|
|
|
|
with self.assertRaises(NoContent):
|
|
Fake.receive_as1({
|
|
'id': 'fake:post',
|
|
'objectType': 'note',
|
|
'author': 'fake:user',
|
|
'published': '2021-12-14T03:04:05+00:00', # NOW - 2w
|
|
})
|
|
self.assertIsNone(Object.get_by_id('fake:post'))
|
|
|
|
self.assertEqual([], Fake.sent)
|
|
self.assertEqual([], OtherFake.sent)
|
|
|
|
def test_too_old_published_without_timezone(self):
|
|
Follower.get_or_create(to=self.user, from_=self.alice)
|
|
|
|
with self.assertRaises(NoContent):
|
|
Fake.receive_as1({
|
|
'id': 'fake:post',
|
|
'objectType': 'note',
|
|
'author': 'fake:user',
|
|
'published': '2021-12-14T03:04:05', # NOW - 2w
|
|
})
|
|
self.assertIsNone(Object.get_by_id('fake:post'))
|
|
|
|
self.assertEqual([], Fake.sent)
|
|
self.assertEqual([], OtherFake.sent)
|
|
|
|
def test_receive_activity_lease(self):
|
|
Follower.get_or_create(to=self.user, from_=self.alice)
|
|
|
|
post_as1 = {
|
|
'objectType': 'activity',
|
|
'verb': 'post',
|
|
'id': 'fake:post',
|
|
'actor': 'fake:user',
|
|
'object': {
|
|
'id': 'fake:note',
|
|
'objectType': 'note',
|
|
'author': 'fake:user',
|
|
'content': 'foo',
|
|
},
|
|
}
|
|
|
|
orig_send = OtherFake.send
|
|
at_send = Condition()
|
|
continue_send = Condition()
|
|
def send(*args, **kwargs):
|
|
with at_send:
|
|
at_send.notify()
|
|
with continue_send:
|
|
continue_send.wait(10) # timeout in seconds
|
|
return orig_send(*args, **kwargs)
|
|
|
|
def receive():
|
|
with app.test_request_context('/'), \
|
|
ndb_client.context(
|
|
cache_policy=common.cache_policy,
|
|
global_cache=_InProcessGlobalCache(),
|
|
global_cache_timeout_policy=common.global_cache_timeout_policy):
|
|
try:
|
|
Fake.receive_as1(post_as1)
|
|
except NoContent: # raised by the second thread
|
|
pass
|
|
|
|
first = Thread(target=receive)
|
|
second = Thread(target=receive)
|
|
|
|
with patch.object(OtherFake, 'send', side_effect=send):
|
|
first.start()
|
|
second.start()
|
|
with at_send:
|
|
at_send.wait(10) # timeout in seconds
|
|
with continue_send:
|
|
continue_send.notify(1)
|
|
first.join()
|
|
second.join()
|
|
|
|
# only one receive call should try to send
|
|
self.assertEqual([('other:alice:target', post_as1)], OtherFake.sent)
|
|
|
|
@patch('protocol.LIMITED_DOMAINS', ['lim.it'])
|
|
@patch('requests.get')
|
|
def test_limited_domain_update_profile_without_follow(self, mock_get):
|
|
actor = {
|
|
**ACTOR,
|
|
'id': 'https://lim.it/alice',
|
|
}
|
|
mock_get.side_effect = [
|
|
self.as2_resp(actor),
|
|
]
|
|
|
|
_, code = got = ActivityPub.receive(Object(our_as1={
|
|
'id': 'https://lim.it/alice#update',
|
|
'objectType': 'activity',
|
|
'verb': 'update',
|
|
'actor': 'https://lim.it/alice',
|
|
'object': actor,
|
|
}), authed_as='https://lim.it/alice')
|
|
self.assertEqual(204, code)
|
|
|
|
self.assert_object('https://lim.it/alice',
|
|
source_protocol='activitypub',
|
|
our_as1=actor,
|
|
users=[ActivityPub(id='https://lim.it/alice').key],
|
|
deleted=False,
|
|
)
|
|
|
|
@patch('protocol.LIMITED_DOMAINS', ['lim.it'])
|
|
@patch.object(ATProto, 'send')
|
|
@patch('requests.get')
|
|
def test_inbox_limited_domain_create_without_follow_no_atproto(
|
|
self, mock_get, mock_send):
|
|
actor = 'https://lim.it/alice'
|
|
user = self.make_user(id=actor, cls=ActivityPub, enabled_protocols=['atproto'])
|
|
|
|
# follow by bot user shouldn't count
|
|
Follower.get_or_create(to=user, from_=Web(id='https://bsky.brid.gy/'))
|
|
|
|
_, code = ActivityPub.receive(Object(as2={
|
|
**NOTE,
|
|
'id': 'https://lim.it/note',
|
|
'actor': actor,
|
|
}), authed_as=actor)
|
|
self.assertEqual(204, code)
|
|
|
|
mock_send.assert_not_called()
|
|
|
|
def test_receive_task_handler_obj_id(self):
|
|
note = {
|
|
'id': 'fake:post',
|
|
'objectType': 'note',
|
|
'author': 'fake:other',
|
|
}
|
|
self.store_object(id='fake:post', our_as1=note, source_protocol='fake')
|
|
|
|
create = {
|
|
'id': 'fake:post#bridgy-fed-create',
|
|
'objectType': 'activity',
|
|
'verb': 'post',
|
|
'object': note,
|
|
'actor': 'fake:other',
|
|
}
|
|
self.store_object(id='fake:post#bridgy-fed-create',
|
|
source_protocol='fake', our_as1=create)
|
|
|
|
resp = self.post('/queue/receive', data={
|
|
'obj_id': 'fake:post#bridgy-fed-create',
|
|
'authed_as': 'fake:other',
|
|
}, headers={'X-AppEngine-TaskRetryCount': '0'})
|
|
self.assertEqual(204, resp.status_code)
|
|
obj = Object.get_by_id('fake:post#bridgy-fed-create')
|
|
|
|
def test_receive_task_handler_properties(self):
|
|
note = {
|
|
'id': 'fake:post',
|
|
'objectType': 'note',
|
|
'author': 'fake:other',
|
|
'content': 'foo',
|
|
}
|
|
create = {
|
|
'id': 'fake:post#bridgy-fed-create',
|
|
'objectType': 'activity',
|
|
'verb': 'post',
|
|
'object': note,
|
|
'actor': 'fake:other',
|
|
}
|
|
|
|
resp = self.post('/queue/receive', data={
|
|
'our_as1': json_dumps(note),
|
|
'source_protocol': 'fake',
|
|
'authed_as': 'fake:other',
|
|
}, headers={'X-AppEngine-TaskRetryCount': '0'})
|
|
self.assertEqual(204, resp.status_code)
|
|
|
|
obj = Object.get_by_id('fake:post')
|
|
self.assertEqual(note, obj.our_as1)
|
|
self.assertIsNone(Object.get_by_id('fake:post#bridgy-fed-create'))
|
|
|
|
@patch.object(Fake, 'receive', side_effect=requests.ConnectionError('foo'))
|
|
def test_receive_task_handler_connection_error(self, _):
|
|
orig_count = Object.query().count()
|
|
got = self.post('/queue/receive', data={
|
|
'our_as1': json_dumps({'id': 'fake:post'}),
|
|
'source_protocol': 'fake',
|
|
})
|
|
self.assertEqual(304, got.status_code)
|
|
self.assertEqual(orig_count, Object.query().count())
|
|
|
|
def test_receive_task_handler_authed_as(self):
|
|
note = {
|
|
'id': 'fake:post',
|
|
'objectType': 'note',
|
|
'author': 'fake:user',
|
|
'content': 'foo',
|
|
}
|
|
|
|
got = self.post('/queue/receive', data={
|
|
'our_as1': json_dumps(note),
|
|
'source_protocol': 'fake',
|
|
'authed_as': 'fake:user',
|
|
})
|
|
self.assertEqual(204, got.status_code)
|
|
self.assertEqual(note, Object.get_by_id('fake:post').our_as1)
|
|
|
|
def test_receive_task_handler_authed_as_domain_vs_homepage(self):
|
|
user = self.make_user('user.com', cls=Web, obj_id='https://user.com/')
|
|
note = {
|
|
'id': 'https://user.com/c',
|
|
'objectType': 'note',
|
|
'author': 'https://user.com/',
|
|
'content': 'foo',
|
|
}
|
|
|
|
got = self.post('/queue/receive', data={
|
|
'our_as1': json_dumps(note),
|
|
'source_protocol': 'web',
|
|
'authed_as': 'user.com',
|
|
})
|
|
self.assertEqual(204, got.status_code)
|
|
self.assertEqual({
|
|
**note,
|
|
'author': 'user.com',
|
|
}, Object.get_by_id('https://user.com/c').our_as1)
|
|
|
|
@patch('requests.get', side_effect=web_user_gets('foo.com') + [ACTOR_HTML_RESP])
|
|
def test_receive_task_handler_authed_as_www_subdomain(self, _):
|
|
note = {
|
|
'id': 'http://www.foo.com/post',
|
|
'objectType': 'note',
|
|
'author': 'http://www.foo.com/bar',
|
|
'content': 'foo',
|
|
}
|
|
|
|
got = self.post('/queue/receive', data={
|
|
'our_as1': json_dumps(note),
|
|
'source_protocol': 'web',
|
|
'authed_as': 'foo.com',
|
|
})
|
|
self.assertEqual(204, got.status_code)
|
|
self.assertEqual(note, Object.get_by_id('http://www.foo.com/post').our_as1)
|
|
|
|
@patch('requests.get', return_value=requests_response('<html></html>'))
|
|
def test_receive_task_handler_authed_as_mixed_subdomains(self, _):
|
|
user = self.make_user('user.com', cls=Web, obj_id='https://user.com/')
|
|
note = {
|
|
'objectType': 'note',
|
|
'id': 'http://user.com/post',
|
|
'author': 'http://m.user.com/',
|
|
'content': 'foo',
|
|
}
|
|
|
|
got = self.post('/queue/receive', data={
|
|
'our_as1': json_dumps(note),
|
|
'source_protocol': 'web',
|
|
'authed_as': 'www.user.com',
|
|
})
|
|
self.assertEqual(204, got.status_code)
|
|
self.assertEqual(note, Object.get_by_id('http://user.com/post').our_as1)
|
|
|
|
@patch('requests.get', return_value=requests_response('<html></html>'))
|
|
def test_receive_task_handler_authed_as_wrong_domain(self, _):
|
|
note = {
|
|
'id': 'http://bar.com/post',
|
|
'objectType': 'note',
|
|
'author': 'http://bar.com/',
|
|
'content': 'foo',
|
|
}
|
|
|
|
got = self.post('/queue/receive', data={
|
|
'our_as1': json_dumps(note),
|
|
'source_protocol': 'web',
|
|
'authed_as': 'foo.com',
|
|
})
|
|
self.assertEqual(299, got.status_code)
|
|
self.assertIsNone(Object.get_by_id('https://bar.com/post'))
|
|
|
|
def test_receive_task_handler_not_authed_as(self):
|
|
note = {
|
|
'id': 'fake:post',
|
|
'objectType': 'note',
|
|
'author': 'fake:other',
|
|
'content': 'foo',
|
|
}
|
|
|
|
got = self.post('/queue/receive', data={
|
|
'our_as1': json_dumps(note),
|
|
'source_protocol': 'fake',
|
|
'authed_as': 'fake:eve',
|
|
})
|
|
self.assertEqual(299, got.status_code)
|
|
self.assertIsNone(Object.get_by_id('fake:post'))
|
|
|
|
def test_like_not_authed_as_actor(self):
|
|
Fake.fetchable['fake:post'] = {
|
|
'objectType': 'note',
|
|
'author': 'other:bob',
|
|
}
|
|
|
|
with self.assertRaises(ErrorButDoNotRetryTask):
|
|
Fake.receive_as1({
|
|
'id': 'fake:like',
|
|
'objectType': 'activity',
|
|
'verb': 'like',
|
|
'actor': 'fake:user',
|
|
'object': 'fake:post',
|
|
}, authed_as='fake:other')
|
|
|
|
self.assertIsNone(Object.get_by_id('fake:like'))
|
|
|
|
def test_user_opted_out(self):
|
|
self.make_followers()
|
|
self.user.obj.our_as1 = {'summary': '#nobridge'}
|
|
self.user.obj.put()
|
|
|
|
with self.assertRaises(NoContent):
|
|
Fake.receive_as1({
|
|
'id': 'fake:post',
|
|
'objectType': 'note',
|
|
'author': 'fake:user',
|
|
})
|
|
|
|
self.assertEqual([], Fake.sent)
|
|
|
|
@patch('oauth_dropins.webutil.appengine_config.tasks_client.create_task')
|
|
def test_from_protocol_unsupported_types(self, mock_create_task):
|
|
common.RUN_TASKS_INLINE = False
|
|
self.make_followers()
|
|
|
|
event = {
|
|
'id': 'fake:event',
|
|
'objectType': 'event',
|
|
'author': 'fake:user',
|
|
}
|
|
post_event = {
|
|
'objectType': 'activity',
|
|
'verb': 'post',
|
|
'id': 'fake:post-event',
|
|
'object': event,
|
|
}
|
|
add = {
|
|
'objectType': 'activity',
|
|
'verb': 'add',
|
|
'id': 'fake:add',
|
|
'actor': 'fake:user',
|
|
'object': 'fake:thing',
|
|
}
|
|
|
|
for activity in event, post_event, add:
|
|
with self.subTest(activity=activity):
|
|
with self.assertRaises(NoContent):
|
|
Fake.receive_as1(activity)
|
|
self.assertEqual([], Fake.sent)
|
|
mock_create_task.assert_not_called()
|
|
|
|
@patch('oauth_dropins.webutil.appengine_config.tasks_client.create_task')
|
|
def test_create_post_send_tasks(self, mock_create_task):
|
|
common.RUN_TASKS_INLINE = False
|
|
self.make_followers()
|
|
|
|
note_as1 = {
|
|
'id': 'fake:post',
|
|
'objectType': 'note',
|
|
'author': 'fake:user',
|
|
'content': 'foo',
|
|
}
|
|
self.assertEqual(('OK', 202), Fake.receive_as1(note_as1))
|
|
|
|
create_as1 = {
|
|
'id': 'fake:post#bridgy-fed-create',
|
|
'objectType': 'activity',
|
|
'verb': 'post',
|
|
'actor': 'fake:user',
|
|
'object': note_as1,
|
|
'published': '2022-01-02T03:04:05+00:00',
|
|
}
|
|
self.assertEqual(2, mock_create_task.call_count)
|
|
self.assert_task(mock_create_task, 'send', source_protocol='fake',
|
|
protocol='other', id='fake:post#bridgy-fed-create',
|
|
our_as1=create_as1, url='other:alice:target',
|
|
user=self.user.key.urlsafe())
|
|
self.assert_task(mock_create_task, 'send', source_protocol='fake',
|
|
protocol='other', id='fake:post#bridgy-fed-create',
|
|
our_as1=create_as1, url='other:bob:target',
|
|
user=self.user.key.urlsafe())
|
|
|
|
self.assertEqual([], OtherFake.sent)
|
|
self.assertEqual([], Fake.sent)
|
|
|
|
@patch('oauth_dropins.webutil.appengine_config.tasks_client.create_task')
|
|
def test_reply_send_tasks_orig_obj(self, mock_create_task):
|
|
common.RUN_TASKS_INLINE = False
|
|
eve = self.make_user('other:eve', cls=OtherFake, obj_id='other:eve')
|
|
|
|
reply_as1 = {
|
|
'id': 'fake:reply',
|
|
'objectType': 'note',
|
|
'inReplyTo': 'other:post',
|
|
'author': 'fake:user',
|
|
'content': 'foo',
|
|
}
|
|
OtherFake.fetchable['other:post'] = {
|
|
'objectType': 'note',
|
|
'id': 'other:post',
|
|
'author': 'other:eve',
|
|
}
|
|
self.assertEqual(('OK', 202), Fake.receive_as1(reply_as1))
|
|
|
|
self.assert_object('fake:reply',
|
|
our_as1=reply_as1,
|
|
type='note',
|
|
users=[self.user.key],
|
|
notify=[eve.key],
|
|
deleted=False,
|
|
)
|
|
|
|
create_as1 = {
|
|
'id': 'fake:reply#bridgy-fed-create',
|
|
'objectType': 'activity',
|
|
'verb': 'post',
|
|
'actor': 'fake:user',
|
|
'object': reply_as1,
|
|
'published': '2022-01-02T03:04:05+00:00',
|
|
}
|
|
self.assert_task(mock_create_task, 'send', source_protocol='fake',
|
|
protocol='other', orig_obj_id='other:post',
|
|
id='fake:reply#bridgy-fed-create', our_as1=create_as1,
|
|
url='other:post:target', user=self.user.key.urlsafe())
|
|
|
|
self.assertEqual([], OtherFake.sent)
|
|
self.assertEqual([], Fake.sent)
|
|
|
|
def test_send_task_handler_obj_id(self):
|
|
self.make_followers()
|
|
|
|
note = self.store_object(id='fake:note', our_as1={
|
|
'id': 'fake:post',
|
|
'objectType': 'note',
|
|
'content': 'foo',
|
|
})
|
|
target = Target(uri='fake:shared:target', protocol='fake')
|
|
create = self.store_object(id='fake:create', our_as1={
|
|
'id': 'fake:create',
|
|
'objectType': 'activity',
|
|
'verb': 'post',
|
|
'actor': 'fake:user',
|
|
'object': note.as1,
|
|
})
|
|
resp = self.post('/queue/send', data={
|
|
'protocol': 'fake',
|
|
'obj_id': 'fake:create',
|
|
'orig_obj_id': 'fake:note',
|
|
'url': 'fake:shared:target',
|
|
'user': self.user.key.urlsafe(),
|
|
}, headers={'X-AppEngine-TaskRetryCount': '0'})
|
|
self.assertEqual(200, resp.status_code)
|
|
|
|
def test_send_task_missing_url(self):
|
|
obj = self.store_object(id='fake:post')
|
|
resp = self.post('/queue/send', data={
|
|
'protocol': 'fake',
|
|
'obj': obj.key.urlsafe(),
|
|
'url': None,
|
|
'user': self.user.key.urlsafe(),
|
|
})
|
|
self.assertEqual(204, resp.status_code)
|
|
|
|
@patch.object(Fake, 'send', return_value=False)
|
|
def test_send_returns_false_task_returns_204(self, mock_send):
|
|
target = Target(protocol='fake', uri='fake:target')
|
|
obj = self.store_object(id='fake:post', our_as1={
|
|
'objectType': 'note',
|
|
})
|
|
resp = self.post('/queue/send', data={
|
|
'protocol': 'fake',
|
|
'obj_id': 'fake:post',
|
|
'url': 'fake:target',
|
|
})
|
|
self.assertEqual(204, resp.status_code)
|
|
|
|
def test_send_unsupported_types(self):
|
|
target = Target(protocol='fake', uri='fake:target')
|
|
|
|
event = {
|
|
'id': 'fake:event',
|
|
'objectType': 'event',
|
|
'author': 'fake:user',
|
|
}
|
|
post_event = {
|
|
'objectType': 'activity',
|
|
'verb': 'post',
|
|
'id': 'fake:post-event',
|
|
'object': event,
|
|
}
|
|
add = {
|
|
'objectType': 'activity',
|
|
'verb': 'add',
|
|
'id': 'fake:add',
|
|
'actor': 'fake:user',
|
|
'object': 'fake:thing',
|
|
}
|
|
|
|
for activity in event, post_event, add:
|
|
with self.subTest(activity=activity):
|
|
self.store_object(id=activity['id'], our_as1=activity)
|
|
resp = self.post('/queue/send', data={
|
|
'protocol': 'fake',
|
|
'obj_id': activity['id'],
|
|
'url': 'fake:target',
|
|
})
|
|
self.assertEqual(204, resp.status_code)
|
|
self.assertEqual([], Fake.sent)
|
|
|
|
@patch.object(Fake, 'send', return_value=True)
|
|
def test_send_task_follow_user_use_instead(self, mock_send):
|
|
self.bob.use_instead = self.alice.key
|
|
self.bob.put()
|
|
|
|
target = Target(uri='fake:target', protocol='fake')
|
|
self.store_object(id='fake:note', our_as1={
|
|
'id': 'fake:post',
|
|
'objectType': 'note',
|
|
'content': 'foo',
|
|
})
|
|
resp = self.post('/queue/send', data={
|
|
'protocol': 'fake',
|
|
'obj_id': 'fake:note',
|
|
'url': 'fake:target',
|
|
'user': self.bob.key.urlsafe(),
|
|
})
|
|
|
|
_, kwargs = mock_send.call_args
|
|
self.assertEqual('other:alice', kwargs['from_user'].key.id())
|