"""Integration tests.""" import copy from unittest.mock import ANY, patch from arroba.datastore_storage import DatastoreStorage from arroba.repo import Repo from arroba.util import dag_cbor_cid, TombstonedRepo from dns.resolver import NXDOMAIN from granary import as2, bluesky from granary.tests.test_bluesky import ACTOR_PROFILE_BSKY, POST_BSKY from oauth_dropins.webutil.flask_util import NoContent from oauth_dropins.webutil.testutil import requests_response from oauth_dropins.webutil import util from oauth_dropins.webutil.util import json_dumps, json_loads from activitypub import ActivityPub import app from atproto import ATProto, Cursor from atproto_firehose import handle, new_commits, Op import common from models import Follower, Object, Target from web import Web from .testutil import ATPROTO_KEY, TestCase from .test_activitypub import ACTOR, add_key, sign from . import test_atproto from . import test_web DID_DOC = { **test_atproto.DID_DOC, 'id': 'did:plc:alice', 'alsoKnownAs': ['at://alice.com'], } PROFILE_GETRECORD = { 'uri': 'at://did:plc:alice/app.bsky.actor.profile/self', 'cid': 'alice sidd', 'value': test_atproto.ACTOR_PROFILE_BSKY, } @patch('ids.COPIES_PROTOCOLS', ['atproto']) class IntegrationTests(TestCase): def setUp(self): super().setUp() self.storage = DatastoreStorage() def make_ap_user(self, ap_id, did=None): user = self.make_user(id=ap_id, cls=ActivityPub, obj_as2=add_key({ 'type': 'Person', 'id': ap_id, 'name': 'My Name', 'image': 'http://pic', 'inbox': f'{ap_id}/inbox', })) if did: self.make_atproto_copy(user, did) return user def make_atproto_user(self, did, enabled_protocols=['activitypub']): self.store_object(id=did, raw=DID_DOC) user = self.make_user(id=did, cls=ATProto, obj_bsky=test_atproto.ACTOR_PROFILE_BSKY, enabled_protocols=enabled_protocols) return user def make_web_user(self, domain, did, enabled_protocols=['activitypub']): ap_subdomain = (domain.removesuffix('.brid.gy') if domain.endswith('.brid.gy') else None) user = self.make_user(id=domain, cls=Web, ap_subdomain=ap_subdomain, enabled_protocols=enabled_protocols) if did: self.make_atproto_copy(user, did) return user def make_atproto_copy(self, user, did): user.enabled_protocols = ['atproto'] user.copies = [Target(uri=did, protocol='atproto')] user.put() Repo.create(self.storage, did, signing_key=ATPROTO_KEY) did_doc = copy.deepcopy(test_atproto.DID_DOC) did_doc['service'][0]['serviceEndpoint'] = ATProto.PDS_URL did_doc['id'] = did self.store_object(id=did, raw=did_doc) if user.obj.as1: profile_id = f'at://{did}/app.bsky.actor.profile/self' self.store_object(id=profile_id, bsky=bluesky.from_as1(user.obj.as1)) user.obj.copies = [Target(uri=profile_id, protocol='atproto')] user.obj.put() @patch('requests.post') def test_atproto_notify_reply_to_activitypub(self, mock_post): """ATProto poll notifications, deliver reply to ActivityPub. ActivityPub original post http://inst/post by bob ATProto reply 123 by alice.com (did:plc:alice) https://github.com/snarfed/bridgy-fed/issues/720 """ alice = self.make_atproto_user('did:plc:alice') bob = self.make_ap_user('http://inst/bob', 'did:plc:bob') self.store_object(id='http://inst/post', source_protocol='activitypub', our_as1={ 'objectType': 'note', 'author': 'http://inst/bob', }, copies=[ Target(uri='at://did:plc:bob/app.bsky.feed.post/123', protocol='atproto'), ]) reply = { '$type': 'app.bsky.feed.post', 'text': 'I hereby reply', 'reply': { 'root': { 'cid': '...', 'uri': 'at://did:plc:bob/app.bsky.feed.post/123', }, 'parent': { 'cid': '...', 'uri': 'at://did:plc:bob/app.bsky.feed.post/123', }, }, } new_commits.put(Op(repo='did:plc:alice', action='create', seq=456, path='app.bsky.feed.post/456', record=reply)) Cursor(id='bgs.local com.atproto.sync.subscribeRepos').put() handle(limit=1) web_test = test_web.WebTest() web_test.user = alice web_test.assert_deliveries(mock_post, ['http://inst/bob/inbox'], data={ '@context': 'https://www.w3.org/ns/activitystreams', 'type': 'Create', 'id': 'https://bsky.brid.gy/convert/ap/at://did:plc:alice/app.bsky.feed.post/456#bridgy-fed-create', 'actor': 'https://bsky.brid.gy/ap/did:plc:alice', 'published': '2022-01-02T03:04:05+00:00', 'object': { 'type': 'Note', 'id': 'https://bsky.brid.gy/convert/ap/at://did:plc:alice/app.bsky.feed.post/456', 'url': 'http://localhost/r/https://bsky.app/profile/did:plc:alice/post/456', 'attributedTo': 'https://bsky.brid.gy/ap/did:plc:alice', 'content': '

I hereby reply

', 'contentMap': {'en': '

I hereby reply

'}, 'content_is_html': True, 'inReplyTo': 'http://inst/post', 'tag': [{'type': 'Mention', 'href': 'http://inst/bob'}], 'to': ['https://www.w3.org/ns/activitystreams#Public'], 'cc': ['http://inst/bob'], }, 'to': ['https://www.w3.org/ns/activitystreams#Public'], }) @patch('requests.post', return_value=requests_response('')) @patch('requests.get', return_value=test_web.WEBMENTION_REL_LINK) def test_atproto_follow_to_web(self, mock_get, mock_post): """ATProto poll notifications, deliver follow to Web. ATProto user alice.com (did:plc:alice) ATProto follow at://did:plc:alice/app.bsky.graph.follow/123 Web user bob.com """ # setup alice = self.make_atproto_user('did:plc:alice', enabled_protocols=['web']) Repo.create(self.storage, 'did:plc:bob', signing_key=ATPROTO_KEY) bob = self.make_user(id='bob.com', cls=Web, copies=[Target(uri='did:plc:bob', protocol='atproto')], enabled_protocols=['atproto']) follow = { '$type': 'app.bsky.graph.follow', 'subject': 'did:plc:bob', 'createdAt': '2022-01-02T03:04:05.000Z', } new_commits.put(Op(repo='did:plc:alice', action='create', seq=123, path='app.bsky.graph.follow/123', record=follow)) Cursor(id='bgs.local com.atproto.sync.subscribeRepos').put() handle(limit=1) self.assert_req(mock_get, 'https://bob.com/') self.assert_req(mock_post, 'https://bob.com/webmention', data={ 'source': 'https://bsky.brid.gy/convert/web/at://did:plc:alice/app.bsky.graph.follow/123', 'target': 'https://bob.com/', }, allow_redirects=False, headers={'Accept': '*/*'}) @patch('dns.resolver.resolve', side_effect=NXDOMAIN()) @patch('oauth_dropins.webutil.appengine_config.tasks_client.create_task') @patch('requests.post', side_effect=[ requests_response('OK'), # create DID ]) @patch('requests.get', side_effect = [ # webmention source page, follow HTML requests_response("""\ """), # https://bob.com/ , for authorship requests_response("""\ Bob """), # alice.com handle resolution, HTTPS method requests_response('did:plc:alice', content_type='text/plain'), # alice profile requests_response(PROFILE_GETRECORD), # alice DID requests_response(DID_DOC), # alice profile requests_response(PROFILE_GETRECORD), ]) def test_web_follow_of_atproto(self, mock_get, mock_post, _, __): """Incoming webmention for a web follow of an ATProto bsky.app profile URL. Web user bob.com ATProto user alice.com (did:plc:alice) Follow is HTML with mf2 u-follow-of of https://bsky.app/profile/alice.com """ bob = self.make_user(id='bob.com', cls=Web, enabled_protocols=['atproto'], obj_mf2={ 'type': ['h-card'], 'properties': { 'url': ['https://bob.com/'], 'name': ['Bob'], }, }) # send webmention resp = self.post('/webmention', data={ 'source': 'https://bob.com/follow', 'target': 'http://localhost', }) self.assertEqual(202, resp.status_code) # check results bob = bob.key.get() self.assertEqual(1, len(bob.copies)) self.assertEqual('atproto', bob.copies[0].protocol) bob_did = bob.copies[0].uri self.assertEqual({ 'type': ['h-entry'], 'properties': { 'url': ['https://bob.com/follow'], 'follow-of': ['https://bsky.app/profile/alice.com'], 'name': [''], 'author': [{ 'type': ['h-card'], 'properties': { 'name': ['Bob'], 'url': ['https://bob.com/'], }, }], }, }, Object.get_by_id('https://bob.com/follow').mf2) repo = self.storage.load_repo('bob.com.web.brid.gy') self.assertEqual(bob_did, repo.did) records = repo.get_contents() self.assertEqual(['app.bsky.actor.profile', 'app.bsky.graph.follow', 'chat.bsky.actor.declaration'], list(records.keys())) self.assertEqual(['self'], list(records['app.bsky.actor.profile'].keys())) self.assertEqual([{ '$type': 'app.bsky.graph.follow', 'subject': 'did:plc:alice', 'createdAt': '2022-01-02T03:04:05.000Z', }], list(records['app.bsky.graph.follow'].values())) @patch('oauth_dropins.webutil.appengine_config.tasks_client.create_task') @patch('requests.get', side_effect=[ # getRecord of original post # alice profile requests_response({ 'uri': 'at://did:plc:alice/app.bsky.feed.post/123', 'cid': 'sydd', 'value': POST_BSKY, }), ]) def test_activitypub_like_of_atproto(self, mock_get, _): """AP inbox delivery of a Like of an ATProto bsky.app profile URL. ActivityPub user @bob@inst , https://inst/bob ATProto user alice.com (did:plc:alice) Like is https://inst/like """ self.make_atproto_user('did:plc:alice') self.make_ap_user('https://inst/bob', 'did:plc:bob') # existing Object with original post, *without* cid. we should generate. Object(id='at://did:plc:alice/app.bsky.feed.post/123', bsky=POST_BSKY).put() # inbox delivery like = { 'type': 'Like', 'id': 'http://inst/like', 'actor': 'https://inst/bob', 'object': 'https://bsky.brid.gy/convert/ap/at://did:plc:alice/app.bsky.feed.post/123', } body = json_dumps(like) headers = sign('/ap/atproto/did:plc:alice/inbox', body, key_id='https://inst/bob') resp = self.client.post('/ap/atproto/did:plc:alice/inbox', data=body, headers=headers) self.assertEqual(202, resp.status_code) # check results self.assertEqual({ **like, # TODO: stop normalizing this in the original protocol's data 'object': 'at://did:plc:alice/app.bsky.feed.post/123', }, Object.get_by_id('http://inst/like').as2) repo = self.storage.load_repo('did:plc:bob') records = repo.get_contents() self.assertEqual(['app.bsky.feed.like'], list(records.keys())) self.assertEqual([{ '$type': 'app.bsky.feed.like', 'subject': { 'uri': 'at://did:plc:alice/app.bsky.feed.post/123', 'cid': dag_cbor_cid(POST_BSKY).encode('base32'), }, 'createdAt': '2022-01-02T03:04:05.000Z', }], list(records['app.bsky.feed.like'].values())) @patch('requests.post', return_value=requests_response('OK')) # create DID @patch('requests.get') def test_activitypub_follow_bsky_bot_user_enables_protocol(self, mock_get, mock_post): """AP follow of @bsky.brid.gy@bsky.brid.gy bridges the account into BLuesky. ActivityPub user @alice@inst , https://inst/alice ATProto bot user bsky.brid.gy (did:plc:bsky) Follow is https://inst/follow """ mock_get.return_value = self.as2_resp(add_key({ 'type': 'Person', 'id': 'https://inst/alice', 'name': 'Mrs. ☕ Alice', 'preferredUsername': 'alice', 'inbox': 'http://inst/inbox', 'image': 'http://pic', })) self.make_user(id='bsky.brid.gy', cls=Web, ap_subdomain='bsky') # deliver follow body = json_dumps({ 'type': 'Follow', 'id': 'http://inst/follow', 'actor': 'https://inst/alice', 'object': 'https://bsky.brid.gy/bsky.brid.gy', }) headers = sign('/bsky.brid.gy/inbox', body, key_id='https://inst/alice') resp = self.client.post('/bsky.brid.gy/inbox', data=body, headers=headers) self.assertEqual(204, resp.status_code) # check results user = ActivityPub.get_by_id('https://inst/alice') self.assertTrue(user.is_enabled(ATProto)) self.assertEqual(1, len(user.copies)) self.assertEqual('atproto', user.copies[0].protocol) did = user.copies[0].uri repo = self.storage.load_repo('alice.inst.ap.brid.gy') self.assertEqual(did, repo.did) records = repo.get_contents() self.assertEqual(['app.bsky.actor.profile', 'chat.bsky.actor.declaration'], list(records.keys())) self.assertEqual(['self'], list(records['app.bsky.actor.profile'].keys())) # bot user DM args, kwargs = mock_post.call_args_list[1] self.assert_equals(('http://inst/inbox',), args) message = '

Welcome to Bridgy Fed! Your account will soon be bridged to Bluesky at alice.inst.ap.brid.gy. See the docs and your user page for more information. To disable this and delete your bridged profile, block this account.

' self.assert_equals({ 'type': 'Note', 'id': 'https://bsky.brid.gy/r/https://bsky.brid.gy/#welcome-dm-https://inst/alice-2022-01-02T03:04:05+00:00', 'actor': 'https://bsky.brid.gy/bsky.brid.gy', 'content': message, 'contentMap': {'en': message}, 'content_is_html': True, 'to': ['https://inst/alice'], }, json_loads(kwargs['data']), ignore=['to', '@context']) # bot user follows back args, kwargs = mock_post.call_args_list[2] self.assert_equals(('http://inst/inbox',), args) self.assert_equals({ 'type': 'Follow', 'id': 'https://bsky.brid.gy/r/https://bsky.brid.gy/#follow-back-https://inst/alice-2022-01-02T03:04:05+00:00', 'actor': 'https://bsky.brid.gy/bsky.brid.gy', 'object': 'https://inst/alice', }, json_loads(kwargs['data']), ignore=['to', '@context']) # accept user's follow args, kwargs = mock_post.call_args_list[3] self.assert_equals(('http://inst/inbox',), args) self.assert_equals({ 'type': 'Accept', 'id': 'http://localhost/r/bsky.brid.gy/followers#accept-http://inst/follow', 'actor': 'https://bsky.brid.gy/bsky.brid.gy', 'object': { 'actor': 'https://inst/alice', 'id': 'http://inst/follow', 'url': 'https://inst/alice#followed-bsky.brid.gy', 'type': 'Follow', 'object': 'https://bsky.brid.gy/bsky.brid.gy', }, }, json_loads(kwargs['data']), ignore=['to', '@context']) @patch('requests.get') def test_activitypub_follow_bsky_bot_bad_username_error(self, mock_get): """AP follow of @bsky.brid.gy@bsky.brid.gy from bad username fails. ActivityPub user @_alice_@inst , https://inst/_alice_ ATProto bot user bsky.brid.gy (did:plc:bsky) Follow is https://inst/follow """ mock_get.return_value = self.as2_resp(add_key({ 'type': 'Person', 'id': 'https://inst/_alice_', 'name': 'Mrs. ☕ Alice', 'preferredUsername': '_alice_', 'inbox': 'http://inst/inbox', })) self.make_user(id='bsky.brid.gy', cls=Web, ap_subdomain='bsky') # deliver follow body = json_dumps({ 'type': 'Follow', 'id': 'http://inst/follow', 'actor': 'https://inst/_alice_', 'object': 'https://bsky.brid.gy/bsky.brid.gy', 'image': 'http://pic', }) headers = sign('/bsky.brid.gy/inbox', body, key_id='https://inst/_alice_') resp = self.client.post('/bsky.brid.gy/inbox', data=body, headers=headers) self.assertEqual(304, resp.status_code) # check results user = ActivityPub.get_by_id('https://inst/_alice_', allow_opt_out=True) self.assertFalse(user.is_enabled(ATProto)) self.assertEqual(0, len(user.copies)) @patch('requests.post', return_value=requests_response({ # sendMessage 'id': 'chat456', 'rev': '22222222tef2d', # ... })) @patch('requests.get', side_effect=[ requests_response(DID_DOC), # alice DID requests_response(PROFILE_GETRECORD), # alice profile requests_response(PROFILE_GETRECORD), # ... requests_response({ # getConvoForMembers 'convo': { 'id': 'convo123', 'rev': '22222222fuozt', }, }), ]) def test_atproto_follow_ap_bot_user_enables_protocol(self, mock_get, mock_post): """ATProto follow of @ap.brid.gy enables the ActivityPub protocol. ATProto user alice.com, did:plc:alice ActivityPub bot user @ap.brid.gy, did:plc:ap """ self.make_web_user('ap.brid.gy', did='did:plc:ap') follow = { '$type': 'app.bsky.graph.follow', 'subject': 'did:plc:ap', } new_commits.put(Op(repo='did:plc:alice', action='create', seq=123, path='app.bsky.graph.follow/123', record=follow)) Cursor(id='bgs.local com.atproto.sync.subscribeRepos').put() handle(limit=1) user = ATProto.get_by_id('did:plc:alice') self.assertTrue(user.is_enabled(ActivityPub)) headers = { 'Content-Type': 'application/json', 'User-Agent': common.USER_AGENT, 'Authorization': ANY, } mock_get.assert_any_call( 'https://chat.service.local/xrpc/chat.bsky.convo.getConvoForMembers?members=did%3Aplc%3Aalice', json=None, data=None, headers=headers) mock_post.assert_called_with( 'https://chat.service.local/xrpc/chat.bsky.convo.sendMessage', json={ 'convoId': 'convo123', 'message': { '$type': 'chat.bsky.convo.defs#messageInput', 'text': 'Welcome to Bridgy Fed! Your account will soon be bridged to the fediverse at @alice.com@bsky.brid.gy. See the docs and your user page for more information. To disable this and delete your bridged profile, block this account.', 'facets': [{ '$type': 'app.bsky.richtext.facet', 'index': {'byteStart': 102, 'byteEnd': 114}, 'features': [{ '$type': 'app.bsky.richtext.facet#link', 'uri': 'https://fed.brid.gy/docs', }], }, { '$type': 'app.bsky.richtext.facet', 'index': {'byteStart': 119, 'byteEnd': 133}, 'features': [{ '$type': 'app.bsky.richtext.facet#link', 'uri': 'https://fed.brid.gy/bsky/alice.com', }], }], 'createdAt': '2022-01-02T03:04:05.000Z', 'bridgyOriginalText': 'Welcome to Bridgy Fed! Your account will soon be bridged to the fediverse at @alice.com@bsky.brid.gy. See the docs and your user page for more information. To disable this and delete your bridged profile, block this account.', 'bridgyOriginalUrl': 'https://ap.brid.gy/#welcome-dm-did:plc:alice-2022-01-02T03:04:05+00:00', }, }, data=None, headers=headers) @patch('requests.post') @patch('requests.get') def test_atproto_block_ap_bot_user_disables_protocol_deletes_actor( self, mock_get, mock_post): """Bluesky user blocks ap.brid.gy: disables protocol, deletes their AP actor. ATProto user alice.com, did:plc:alice """ self.make_web_user('ap.brid.gy', did='did:plc:ap') alice = self.make_atproto_user('did:plc:alice') Follower.get_or_create(to=alice, from_=self.make_ap_user('http://x/bob')) Follower.get_or_create(to=alice, from_=self.make_ap_user('http://y/eve')) block = { '$type': 'app.bsky.graph.block', 'subject': 'did:plc:ap', 'createdAt': '2022-01-02T03:04:05.000Z' } new_commits.put(Op(repo='did:plc:alice', action='create', seq=123, path='app.bsky.graph.block/123', record=block)) Cursor(id='bgs.local com.atproto.sync.subscribeRepos').put() handle(limit=1) self.assertEqual(2, mock_post.call_count) args, kwargs = mock_post.call_args_list self.assertEqual([('http://x/bob/inbox',), ('http://y/eve/inbox',)], [args for args, _ in mock_post.call_args_list]) for _, kwargs in mock_post.call_args_list: self.assert_equals({ '@context': 'https://www.w3.org/ns/activitystreams', 'type': 'Delete', 'id': 'https://bsky.brid.gy/convert/ap/at://did:plc:alice/app.bsky.actor.profile/self#delete-copy-activitypub-2022-01-02T03:04:05+00:00', 'actor': 'https://bsky.brid.gy/ap/did:plc:alice', 'object': 'https://bsky.brid.gy/ap/did:plc:alice', 'to': ['https://www.w3.org/ns/activitystreams#Public'], }, json_loads(kwargs['data'])) @patch('requests.get', return_value=requests_response('')) # alice, http://pic/ def test_activitypub_block_bsky_bot_user_tombstones_atproto_repo(self, mock_get): """AP follow of @bsky.brid.gy@bsky.brid.gy bridges the account into Bluesky. ActivityPub user @alice@inst , https://inst/alice , did:plc:alice Block is https://inst/block """ u = self.make_ap_user('https://inst/alice', 'did:plc:alice') self.make_user(id='bsky.brid.gy', cls=Web, ap_subdomain='bsky') # deliver block body = json_dumps({ 'type': 'Block', 'id': 'http://inst/block', 'actor': 'https://inst/alice', 'object': 'https://bsky.brid.gy/bsky.brid.gy', }) headers = sign('/bsky.brid.gy/inbox', body, key_id='https://inst/alice') resp = self.client.post('/bsky.brid.gy/inbox', data=body, headers=headers) self.assertEqual(200, resp.status_code) # check results user = ActivityPub.get_by_id('https://inst/alice') self.assertFalse(user.is_enabled(ATProto)) with self.assertRaises(TombstonedRepo): self.storage.load_repo('did:plc:alice') @patch('requests.post') @patch('requests.get') def test_atproto_mention_activitypub(self, mock_get, mock_post): """Bluesky @-mention of *.ap.brid.gy user. ATProto user alice.com, did:plc:alice ActivityPub user bob@inst, https://inst/bob, bob.inst.ap.brid.gy, did:plc:bob """ alice = self.make_atproto_user('did:plc:alice') bob = self.make_ap_user('https://inst/bob', 'did:plc:bob') post = { '$type': 'app.bsky.feed.post', 'text': 'maybe if @bob.inst.ap.brid.gy and Alf meet up', 'facets': [{ '$type': 'app.bsky.richtext.facet', 'features': [{ '$type': 'app.bsky.richtext.facet#mention', 'did': 'did:plc:bob', }], 'index': { 'byteEnd': 29, 'byteStart': 9, }, }], } new_commits.put(Op(repo='did:plc:alice', action='create', seq=123, path='app.bsky.feed.post/123', record=post)) Cursor(id='bgs.local com.atproto.sync.subscribeRepos').put() handle(limit=1) self.assertEqual(1, mock_post.call_count) args, kwargs = mock_post.call_args self.assertEqual((bob.obj.as2['inbox'],), args) self.assert_equals({ '@context': 'https://www.w3.org/ns/activitystreams', 'type': 'Create', 'id': 'https://bsky.brid.gy/convert/ap/at://did:plc:alice/app.bsky.feed.post/123#bridgy-fed-create', 'actor': 'https://bsky.brid.gy/ap/did:plc:alice', 'published': '2022-01-02T03:04:05+00:00', 'object': { 'type': 'Note', 'id': 'https://bsky.brid.gy/convert/ap/at://did:plc:alice/app.bsky.feed.post/123', 'url': 'http://localhost/r/https://bsky.app/profile/did:plc:alice/post/123', 'attributedTo': 'https://bsky.brid.gy/ap/did:plc:alice', 'content': '

maybe if @bob.inst.ap.brid.gy and Alf meet up

', 'content_is_html': True, 'tag': [{ 'type': 'Mention', 'name': '@bob.inst.ap.brid.gy', 'href': 'https://inst/bob', }], }, }, json_loads(kwargs['data']), ignore=['@context', 'contentMap', 'to', 'cc']) @patch('requests.post') @patch('requests.get') def test_atproto_undo_block_of_activitypub(self, mock_get, mock_post): """Bluesky undo of a block of an AP user. ATProto user alice.com, did:plc:alice ActivityPub user bob@inst, https://inst/bob, bob.inst.ap.brid.gy, did:plc:bob """ alice = self.make_atproto_user('did:plc:alice') bob = self.make_ap_user('https://inst/bob', 'did:plc:bob') o = self.store_object(id='at://did:plc:alice/app.bsky.graph.block/123', source_protocol='atproto', our_as1={ 'objectType': 'activity', 'verb': 'block', 'id': 'at://did:plc:alice/app.bsky.graph.block/123', 'object': 'https://inst/bob', 'actor': 'did:plc:alice', }) new_commits.put(Op(repo='did:plc:alice', action='delete', seq=123, path='app.bsky.graph.block/123')) Cursor(id='bgs.local com.atproto.sync.subscribeRepos').put() handle(limit=1) self.assertEqual(1, mock_post.call_count) args, kwargs = mock_post.call_args self.assertEqual((bob.obj.as2['inbox'],), args) self.assert_equals({ '@context': 'https://www.w3.org/ns/activitystreams', 'type': 'Undo', 'id': 'https://bsky.brid.gy/convert/ap/at://did:plc:alice/app.bsky.graph.block/123#undo', 'actor': 'https://bsky.brid.gy/ap/did:plc:alice', 'object': 'https://bsky.brid.gy/convert/ap/at://did:plc:alice/app.bsky.graph.block/123', }, json_loads(kwargs['data']), ignore=['@context', 'contentMap', 'to', 'cc']) @patch('requests.post', side_effect=[ requests_response('OK'), # create DID requests_response({'id': 3}), # createReport ]) @patch('requests.get', side_effect=[ requests_response(PROFILE_GETRECORD), requests_response(DID_DOC), ]) def test_activitypub_server_actor_flag_to_atproto_report( self, mock_get, mock_post): """AP Flag activity from server actor translates to Bluesky report. ActivityPub user @actor@inst , https://inst/actor , did:plc:actor creates new ATProto repo for them """ self.make_user(id='https://inst/actor', cls=ActivityPub, obj_as2=add_key({ 'type': 'Application', 'id': 'https://inst/actor', 'preferredUsername': 'inst', })) # deliver flag body = json_dumps({ 'type': 'Flag', 'id': 'http://inst/flag', 'actor': 'https://inst/actor', 'object': 'https://bsky.brid.gy/convert/ap/at://did:plc:alice/app.bsky.actor.profile/self', }) headers = sign('/bsky.brid.gy/inbox', body, key_id='https://inst/actor') resp = self.client.post('/bsky.brid.gy/inbox', data=body, headers=headers) self.assertEqual(202, resp.status_code) # check results user = ActivityPub.get_by_id('https://inst/actor') self.assertTrue(user.is_enabled(ATProto)) mock_post.assert_called_with( 'https://mod.service.local/xrpc/com.atproto.moderation.createReport', json={ '$type': 'com.atproto.moderation.createReport#input', 'reasonType': 'com.atproto.moderation.defs#reasonOther', 'reason': '', 'subject': { '$type': 'com.atproto.repo.strongRef', 'uri': 'at://did:plc:alice/app.bsky.actor.profile/self', 'cid': 'alice sidd', }, }, data=None, headers=ANY)