def test_activitypub_create_reply(self, mock_get, mock_post): mock_get.side_effect = self.activitypub_gets mock_post.return_value = requests_response('abc xyz', status=203) got = application.get_response('/webmention', method='POST', body=urlencode({ 'source': 'http://a/reply', 'target': 'https://fed.brid.gy/', }).encode()) self.assertEqual(203, got.status_int) mock_get.assert_has_calls(( self.req('http://a/reply'), self.req('http://orig/post', headers=CONNEG_HEADERS_AS2_HTML), self.req('http://orig/author', headers=CONNEG_HEADERS_AS2_HTML), )) args, kwargs = mock_post.call_args self.assertEqual(('https://foo.com/inbox', ), args) self.assertEqual(self.as2_create, kwargs['json']) headers = kwargs['headers'] self.assertEqual(CONTENT_TYPE_AS2, headers['Content-Type']) rsa_key = kwargs['auth'].header_signer._rsa._key self.assertEqual(self.key.private_pem(), rsa_key.exportKey()) resp = Response.get_by_id('http://a/reply http://orig/as2') self.assertEqual('out', resp.direction) self.assertEqual('activitypub', resp.protocol) self.assertEqual('complete', resp.status) self.assertEqual(self.reply_mf2, json_loads(resp.source_mf2))
def get(self): source = util.get_required_param(self, 'source') target = util.get_required_param(self, 'target') id = '%s %s' % (source, target) resp = Response.get_by_id(id) if not resp: self.abort(404, 'No stored response for %s' % id) if resp.source_mf2: as1 = microformats2.json_to_object(json.loads(resp.source_mf2)) elif resp.source_as2: as1 = as2.to_as1(json.loads(resp.source_as2)) elif resp.source_atom: as1 = atom.atom_to_activity(resp.source_atom) else: self.abort(404, 'Stored response for %s has no data' % id) # add HTML meta redirect to source page. should trigger for end users in # browsers but not for webmention receivers (hopefully). html = microformats2.activities_to_html([as1]) utf8 = '<meta charset="utf-8">' refresh = '<meta http-equiv="refresh" content="0;url=%s">' % source html = html.replace(utf8, utf8 + '\n' + refresh) self.response.write(html)
def test_like(self, mock_urlopen, mock_head, mock_get, mock_post): atom_like = """\ <?xml version='1.0' encoding='UTF-8'?> <entry xmlns='http://www.w3.org/2005/Atom' xmlns:activity='http://activitystrea.ms/spec/1.0/'> <uri>https://my/like</uri> <author> <name>Alice</name> <uri>[email protected]</uri> </author> <activity:verb>http://activitystrea.ms/schema/1.0/like</activity:verb> <activity:object>http://orig/post</activity:object> <updated>%s</updated> </entry>""" % datetime.datetime.now().isoformat('T') self.send_slap(mock_urlopen, mock_head, mock_get, mock_post, atom_like) # check webmention post mock_post.assert_called_once_with( 'http://orig/webmention', data={ 'source': 'http://localhost/render?source=https%3A%2F%2Fmy%2Flike&target=http%3A%2F%2Forig%2Fpost', 'target': 'http://orig/post', }, allow_redirects=False, headers=self.expected_headers, verify=False) # check stored response resp = Response.get_by_id('https://my/like http://orig/post') self.assertEqual('in', resp.direction) self.assertEqual('ostatus', resp.protocol) self.assertEqual('complete', resp.status) self.assertEqual(atom_like, resp.source_atom)
def test_activitypub_follow(self, mock_get, mock_post): mock_get.side_effect = [self.follow, self.actor] mock_post.return_value = requests_response('abc xyz') got = app.get_response('/webmention', method='POST', body=urllib.urlencode({ 'source': 'http://a/follow', 'target': 'https://fed.brid.gy/', })) self.assertEquals(200, got.status_int) mock_get.assert_has_calls(( self.req('http://a/follow'), self.req('http://followee', headers=CONNEG_HEADERS_AS2_HTML), )) args, kwargs = mock_post.call_args self.assertEqual(('https://foo.com/inbox', ), args) self.assertEqual(self.follow_as2, kwargs['json']) headers = kwargs['headers'] self.assertEqual(CONTENT_TYPE_AS2, headers['Content-Type']) rsa_key = kwargs['auth'].header_signer._rsa._key self.assertEqual(self.key.private_pem(), rsa_key.exportKey()) resp = Response.get_by_id('http://a/follow http://followee') self.assertEqual('out', resp.direction) self.assertEqual('activitypub', resp.protocol) self.assertEqual('complete', resp.status) self.assertEqual(self.follow_mf2, json.loads(resp.source_mf2))
def test_reply(self, mock_urlopen, mock_head, mock_get, mock_post): atom_reply = """\ <?xml version='1.0' encoding='UTF-8'?> <entry xmlns='http://www.w3.org/2005/Atom'> <id>https://my/reply</id> <uri>https://my/reply</uri> <author> <name>Alice</name> <uri>[email protected]</uri> </author> <thr:in-reply-to xmlns:thr="http://purl.org/syndication/thread/1.0"> http://orig/post </thr:in-reply-to> <content>I hereby reply.</content> <title>My Reply</title> <updated>%s</updated> </entry>""" % datetime.datetime.now().isoformat('T') self.send_slap(mock_urlopen, mock_head, mock_get, mock_post, atom_reply) # check webmention post mock_post.assert_called_once_with( 'http://orig/webmention', data={'source': 'https://my/reply', 'target': 'http://orig/post'}, allow_redirects=False, timeout=15, stream=True, headers=self.expected_headers) # check stored response resp = Response.get_by_id('https://my/reply http://orig/post') self.assertEqual('in', resp.direction) self.assertEqual('ostatus', resp.protocol) self.assertEqual('complete', resp.status) self.assertEqual(atom_reply, resp.source_atom)
def _test_inbox_mention(self, as2, mock_head, mock_get, mock_post): mock_head.return_value = requests_response(url='http://target') mock_get.return_value = requests_response( '<html><head><link rel="webmention" href="/webmention"></html>') mock_post.return_value = requests_response() with self.client: got = self.client.post('/foo.com/inbox', json=as2) self.assertEqual(200, got.status_code, got.get_data(as_text=True)) mock_get.assert_called_once_with( 'http://target/', headers=common.HEADERS, timeout=15, stream=True) expected_headers = copy.deepcopy(common.HEADERS) expected_headers['Accept'] = '*/*' mock_post.assert_called_once_with( 'http://target/webmention', data={ 'source': 'http://localhost/render?source=http%3A%2F%2Fthis%2Fmention&target=http%3A%2F%2Ftarget%2F', 'target': 'http://target/', }, allow_redirects=False, timeout=15, stream=True, headers=expected_headers) resp = Response.get_by_id('http://this/mention http://target/') self.assertEqual('in', resp.direction) self.assertEqual('activitypub', resp.protocol) self.assertEqual('complete', resp.status) self.assertEqual(common.redirect_unwrap(as2), json_loads(resp.source_as2))
def test_salmon_like(self, mock_get, mock_post): mock_get.side_effect = [self.like, self.orig_html_atom, self.orig_atom] got = self.client.post('/webmention', data={ 'source': 'http://a/like', 'target': 'http://orig/post', }) self.assertEqual(200, got.status_code) mock_get.assert_has_calls(( self.req('http://a/like'), self.req('http://orig/post', headers=CONNEG_HEADERS_AS2_HTML), self.req('http://orig/atom'), )) data = self.verify_salmon(mock_post) parsed = feedparser.parse(data) entry = parsed.entries[0] self.assertEqual('http://a/like', entry['id']) self.assertIn( { 'rel': 'alternate', 'href': 'http://a/like', 'type': 'text/html', }, entry['links']) self.assertEqual('http://orig/post', entry['activity_object']) resp = Response.get_by_id('http://a/like http://orig/post') self.assertEqual('out', resp.direction) self.assertEqual('ostatus', resp.protocol) self.assertEqual('complete', resp.status) self.assertEqual(self.like_mf2, json_loads(resp.source_mf2))
def test_activitypub_create_repost(self, mock_get, mock_post): mock_get.side_effect = [self.repost, self.orig_as2, self.actor] mock_post.return_value = requests_response('abc xyz') got = self.client.post('/webmention', data={ 'source': 'http://a/repost', 'target': 'https://fed.brid.gy/', }) self.assertEqual(200, got.status_code) mock_get.assert_has_calls(( self.req('http://a/repost'), self.req('http://orig/post', headers=CONNEG_HEADERS_AS2_HTML), self.req('http://orig/author', headers=CONNEG_HEADERS_AS2_HTML), )) args, kwargs = mock_post.call_args self.assertEqual(('https://foo.com/inbox', ), args) self.assertEqual(self.repost_as2, json_loads(kwargs['data'])) headers = kwargs['headers'] self.assertEqual(CONTENT_TYPE_AS2, headers['Content-Type']) rsa_key = kwargs['auth'].header_signer._rsa._key self.assertEqual(self.key.private_pem(), rsa_key.exportKey()) resp = Response.get_by_id('http://a/repost http://orig/as2') self.assertEqual('out', resp.direction) self.assertEqual('activitypub', resp.protocol) self.assertEqual('complete', resp.status) self.assertEqual(self.repost_mf2, json_loads(resp.source_mf2))
def test_inbox_like(self, mock_head, mock_get, mock_post): mock_head.return_value = requests_response(url='http://orig/post') mock_get.side_effect = [ # source actor requests_response(LIKE_WITH_ACTOR['actor'], headers={'Content-Type': common.CONTENT_TYPE_AS2}), # target post webmention discovery requests_response( '<html><head><link rel="webmention" href="/webmention"></html>'), ] mock_post.return_value = requests_response() got = self.client.post('/foo.com/inbox', json=LIKE) self.assertEqual(200, got.status_code) as2_headers = copy.deepcopy(common.HEADERS) as2_headers.update(common.CONNEG_HEADERS_AS2_HTML) mock_get.assert_has_calls(( call('http://orig/actor', headers=as2_headers, stream=True, timeout=15), call('http://orig/post', headers=common.HEADERS, stream=True, timeout=15), )) args, kwargs = mock_post.call_args self.assertEqual(('http://orig/webmention',), args) self.assertEqual({ # TODO 'source': 'http://localhost/render?source=http%3A%2F%2Fthis%2Flike__ok&target=http%3A%2F%2Forig%2Fpost', 'target': 'http://orig/post', }, kwargs['data']) resp = Response.get_by_id('http://this/like__ok http://orig/post') self.assertEqual('in', resp.direction) self.assertEqual('activitypub', resp.protocol) self.assertEqual('complete', resp.status) self.assertEqual(LIKE_WITH_ACTOR, json_loads(resp.source_as2))
def render(): """Fetches a stored Response and renders it as HTML.""" source = flask_util.get_required_param('source') target = flask_util.get_required_param('target') id = f'{source} {target}' resp = Response.get_by_id(id) if not resp: error(f'No stored response for {id}', status=404) if resp.source_mf2: as1 = microformats2.json_to_object(json_loads(resp.source_mf2)) elif resp.source_as2: as1 = as2.to_as1(json_loads(resp.source_as2)) elif resp.source_atom: as1 = atom.atom_to_activity(resp.source_atom) else: error(f'Stored response for {id} has no data', status=404) # add HTML meta redirect to source page. should trigger for end users in # browsers but not for webmention receivers (hopefully). html = microformats2.activities_to_html([as1]) utf8 = '<meta charset="utf-8">' refresh = f'<meta http-equiv="refresh" content="0;url={source}">' return html.replace(utf8, utf8 + '\n' + refresh)
def _test_inbox_mention(self, as2, mock_head, mock_get, mock_post): mock_head.return_value = requests_response(url='http://target') mock_get.return_value = requests_response( '<html><head><link rel="webmention" href="/webmention"></html>') mock_post.return_value = requests_response() got = app.get_response('/foo.com/inbox', method='POST', body=json.dumps(as2)) self.assertEquals(200, got.status_int, got.body) mock_get.assert_called_once_with('http://target/', headers=common.HEADERS, verify=False) expected_headers = copy.deepcopy(common.HEADERS) expected_headers['Accept'] = '*/*' mock_post.assert_called_once_with( 'http://target/webmention', data={ 'source': 'http://localhost/render?source=http%3A%2F%2Fthis%2Fmention&target=http%3A%2F%2Ftarget%2F', 'target': 'http://target/', }, allow_redirects=False, headers=expected_headers, verify=False) resp = Response.get_by_id('http://this/mention http://target/') self.assertEqual('in', resp.direction) self.assertEqual('activitypub', resp.protocol) self.assertEqual('complete', resp.status) self.assertEqual(common.redirect_unwrap(as2), json.loads(resp.source_as2))
def test_activitypub_create_post(self, mock_get, mock_post): mock_get.side_effect = [self.create, self.actor] mock_post.return_value = requests_response('abc xyz') Follower.get_or_create('orig', 'https://mastodon/aaa') Follower.get_or_create('orig', 'https://mastodon/bbb', last_follow=json.dumps({ 'actor': { 'publicInbox': 'https://public/inbox', 'inbox': 'https://unused', } })) Follower.get_or_create('orig', 'https://mastodon/ccc', last_follow=json.dumps({ 'actor': { 'endpoints': { 'sharedInbox': 'https://shared/inbox', }, } })) Follower.get_or_create('orig', 'https://mastodon/ddd', last_follow=json.dumps( {'actor': { 'inbox': 'https://inbox', }})) self.datastore_stub.Flush() got = app.get_response('/webmention', method='POST', body=urllib.urlencode({ 'source': 'http://orig/post', 'target': 'https://fed.brid.gy/', })) self.assertEquals(200, got.status_int) mock_get.assert_has_calls((self.req('http://orig/post'), )) inboxes = ('https://public/inbox', 'https://shared/inbox', 'https://inbox') for call, inbox in zip(mock_post.call_args_list, inboxes): self.assertEquals((inbox, ), call[0]) self.assertEquals(self.create_as2, call[1]['json']) for inbox in inboxes: resp = Response.get_by_id('http://orig/post %s' % inbox) self.assertEqual('out', resp.direction, inbox) self.assertEqual('activitypub', resp.protocol, inbox) self.assertEqual('complete', resp.status, inbox) self.assertEqual(self.create_mf2, json.loads(resp.source_mf2), inbox)
def test_inbox_follow_accept(self, mock_head, mock_get, mock_post): mock_head.return_value = requests_response(url='https://realize.be/') mock_get.side_effect = [ # source actor requests_response(FOLLOW_WITH_ACTOR['actor'], content_type=common.CONTENT_TYPE_AS2), # target post webmention discovery requests_response( '<html><head><link rel="webmention" href="/webmention"></html>' ), ] mock_post.return_value = requests_response() got = application.get_response( '/foo.com/inbox', method='POST', body=json_dumps(FOLLOW_WRAPPED).encode()) self.assertEqual(200, got.status_int) as2_headers = copy.deepcopy(common.HEADERS) as2_headers.update(common.CONNEG_HEADERS_AS2_HTML) mock_get.assert_has_calls((call(FOLLOW['actor'], headers=as2_headers, stream=True, timeout=15), )) # check AP Accept self.assertEqual(2, len(mock_post.call_args_list)) args, kwargs = mock_post.call_args_list[0] self.assertEqual(('http://follower/inbox', ), args) self.assertEqual(ACCEPT, kwargs['json']) # check webmention args, kwargs = mock_post.call_args_list[1] self.assertEqual(('https://realize.be/webmention', ), args) self.assertEqual( { 'source': 'http://localhost/render?source=https%3A%2F%2Fmastodon.social%2F6d1a&target=https%3A%2F%2Frealize.be%2F', 'target': 'https://realize.be/', }, kwargs['data']) resp = Response.get_by_id( 'https://mastodon.social/6d1a https://realize.be/') self.assertEqual('in', resp.direction) self.assertEqual('activitypub', resp.protocol) self.assertEqual('complete', resp.status) self.assertEqual(FOLLOW_WITH_ACTOR, json_loads(resp.source_as2)) # check that we stored a Follower object follower = Follower.get_by_id('realize.be %s' % (FOLLOW['actor'])) self.assertEqual('active', follower.status) self.assertEqual(FOLLOW_WRAPPED_WITH_ACTOR, json_loads(follower.last_follow))
def test_salmon_reply(self, mock_get, mock_post): mock_get.side_effect = [ self.reply, self.not_fediverse, self.orig_html_atom, self.orig_atom ] got = application.get_response('/webmention', method='POST', body=urlencode({ 'source': 'http://a/reply', 'target': 'http://orig/post', }).encode()) self.assertEqual(200, got.status_int) mock_get.assert_has_calls(( self.req('http://a/reply'), self.req('http://not/fediverse', headers=CONNEG_HEADERS_AS2_HTML), self.req('http://orig/post', headers=CONNEG_HEADERS_AS2_HTML), self.req('http://orig/atom'), )) data = self.verify_salmon(mock_post) parsed = feedparser.parse(data) entry = parsed.entries[0] self.assertEqual('http://a/reply', entry['id']) self.assertIn( { 'rel': 'alternate', 'href': 'http://a/reply', 'type': 'text/html', }, entry['links']) self.assertEqual( { 'type': 'text/html', 'href': 'http://orig/post', 'ref': 'tag:fed.brid.gy,2017-08-22:orig-post', }, entry['thr_in-reply-to']) self.assertEqual( """\ <a class="u-in-reply-to" href="http://not/fediverse"></a><br></br> <a class="u-in-reply-to" href="http://orig/post">foo ☕ bar</a><br></br> <a href="http://localhost/"></a>""", entry.content[0]['value']) resp = Response.get_by_id('http://a/reply http://orig/post') self.assertEqual('out', resp.direction) self.assertEqual('ostatus', resp.protocol) self.assertEqual('complete', resp.status) self.assertEqual(self.reply_mf2, json_loads(resp.source_mf2))
def test_inbox_no_webmention_endpoint(self, mock_head, mock_get, mock_post): mock_get.side_effect = [ # source actor requests_response(LIKE_WITH_ACTOR['actor'], headers={'Content-Type': common.CONTENT_TYPE_AS2}), # target post webmention discovery requests_response('<html><body>foo</body></html>'), ] got = self.client.post('/foo.com/inbox', json=LIKE) self.assertEqual(200, got.status_code) resp = Response.get_by_id('http://this/like__ok http://orig/post') self.assertEqual('in', resp.direction) self.assertEqual('activitypub', resp.protocol) self.assertEqual('ignored', resp.status)
def get(self): source = util.get_required_param(self, 'source') target = util.get_required_param(self, 'target') id = '%s %s' % (source, target) resp = Response.get_by_id(id) if not resp: self.abort(404, 'No stored response for %s' % id) if resp.source_mf2: as1 = microformats2.json_to_object(json.loads(resp.source_mf2)) elif resp.source_as2: as1 = as2.to_as1(json.loads(resp.source_as2)) elif resp.source_atom: as1 = atom.atom_to_activity(resp.source_atom) else: self.abort(404, 'Stored response for %s has no data' % id) self.response.write(microformats2.activities_to_html([as1]))
def test_salmon_no_target_atom(self, mock_get, mock_post): orig_no_atom = requests_response( """\ <html> <body>foo</body> </html>""", 'http://orig/url') mock_get.side_effect = [self.reply, orig_no_atom] got = app.get_response('/webmention', method='POST', body=urllib.urlencode({ 'source': 'http://a/reply', 'target': 'http://orig/post', })) self.assertEquals(400, got.status_int) self.assertIn('Target post http://orig/url has no Atom link', got.body) self.assertIsNone( Response.get_by_id('http://a/reply http://orig/post'))
def test_salmon_no_target_atom(self, mock_get, mock_post): orig_no_atom = requests_response( """\ <html> <body>foo</body> </html>""", 'http://orig/url') mock_get.side_effect = [self.reply, self.not_fediverse, orig_no_atom] got = self.client.post('/webmention', data={ 'source': 'http://a/reply', 'target': 'http://orig/post', }) self.assertEqual(400, got.status_code) self.assertIn('Target post http://orig/url has no Atom link', got.get_data(as_text=True)) resp = Response.get_by_id('http://a/reply http://orig/url') self.assertEqual('out', resp.direction) self.assertEqual('ostatus', resp.protocol) self.assertEqual('error', resp.status)
def test_activitypub_error_no_salmon_fallback(self, mock_get, mock_post): mock_get.side_effect = [self.follow, self.actor] mock_post.return_value = requests_response('abc xyz', status=405, url='https://foo.com/inbox') got = application.get_response('/webmention', method='POST', body=urlencode({ 'source': 'http://a/follow', 'target': 'https://fed.brid.gy/', }).encode()) self.assertEqual(502, got.status_int, got.text) self.assertEqual( '405 Client Error: None for url: https://foo.com/inbox ; abc xyz', got.text) mock_get.assert_has_calls(( self.req('http://a/follow'), self.req('http://followee/', headers=CONNEG_HEADERS_AS2_HTML), )) args, kwargs = mock_post.call_args self.assertEqual(('https://foo.com/inbox', ), args) self.assertEqual(self.follow_as2, json_loads(kwargs['data'])) headers = kwargs['headers'] self.assertEqual(CONTENT_TYPE_AS2, headers['Content-Type']) rsa_key = kwargs['auth'].header_signer._rsa._key self.assertEqual(self.key.private_pem(), rsa_key.exportKey()) resp = Response.get_by_id('http://a/follow http://followee/') self.assertEqual('out', resp.direction) self.assertEqual('activitypub', resp.protocol) self.assertEqual('error', resp.status) self.assertEqual(self.follow_mf2, json_loads(resp.source_mf2))
def test_inbox_reply(self, mock_get, mock_post): mock_get.return_value = requests_response( '<html><head><link rel="webmention" href="/webmention"></html>') mock_post.return_value = requests_response() as2_note = { '@context': 'https://www.w3.org/ns/activitystreams', 'type': 'Note', 'content': 'A ☕ reply', 'url': 'http://this/reply', 'inReplyTo': 'http://orig/post', 'cc': ['https://www.w3.org/ns/activitystreams#Public'], } got = app.get_response('/foo.com/inbox', method='POST', body=json.dumps(as2_note)) self.assertEquals(200, got.status_int, got.body) mock_get.assert_called_once_with('http://orig/post', headers=common.HEADERS, verify=False) expected_headers = copy.deepcopy(common.HEADERS) expected_headers['Accept'] = '*/*' mock_post.assert_called_once_with('http://orig/webmention', data={ 'source': 'http://this/reply', 'target': 'http://orig/post', }, allow_redirects=False, headers=expected_headers, verify=False) resp = Response.get_by_id('http://this/reply http://orig/post') self.assertEqual('in', resp.direction) self.assertEqual('activitypub', resp.protocol) self.assertEqual('complete', resp.status) self.assertEqual(as2_note, json.loads(resp.source_as2))
def test_inbox_like_proxy_url(self, mock_get, mock_post): actor = { '@context': 'https://www.w3.org/ns/activitystreams', 'id': 'http://orig/actor', 'type': 'Person', 'name': 'Ms. Actor', 'preferredUsername': '******', 'image': { 'type': 'Image', 'url': 'http://orig/pic.jpg' }, } mock_get.side_effect = [ # source actor requests_response( actor, headers={'Content-Type': common.CONTENT_TYPE_AS2}), # target post webmention discovery requests_response( '<html><head><link rel="webmention" href="/webmention"></html>' ), ] mock_post.return_value = requests_response() # based on example Mastodon like: # https://github.com/snarfed/bridgy-fed/issues/4#issuecomment-334212362 # (reposts are very similar) as2_like = { '@context': 'https://www.w3.org/ns/activitystreams', 'id': 'http://this/like#ok', 'type': 'Like', 'object': 'http://orig/post', 'actor': 'http://orig/actor', } got = app.get_response('/foo.com/inbox', method='POST', body=json.dumps(as2_like)) self.assertEquals(200, got.status_int) as2_headers = copy.deepcopy(common.HEADERS) as2_headers.update(common.CONNEG_HEADERS_AS2_HTML) mock_get.assert_has_calls(( call('http://orig/actor', headers=as2_headers, timeout=15), call('http://orig/post', headers=common.HEADERS, verify=False), )) args, kwargs = mock_post.call_args self.assertEquals(('http://orig/webmention', ), args) self.assertEquals( { # TODO 'source': 'http://localhost/render?source=http%3A%2F%2Fthis%2Flike__ok&target=http%3A%2F%2Forig%2Fpost', 'target': 'http://orig/post', }, kwargs['data']) resp = Response.get_by_id('http://this/like__ok http://orig/post') self.assertEqual('in', resp.direction) self.assertEqual('activitypub', resp.protocol) self.assertEqual('complete', resp.status) as2_like['actor'] = actor self.assertEqual(as2_like, json.loads(resp.source_as2))
def test_activitypub_create_post(self, mock_get, mock_post): mock_get.side_effect = [self.create, self.actor] mock_post.return_value = requests_response('abc xyz') Follower.get_or_create('orig', 'https://mastodon/aaa') Follower.get_or_create('orig', 'https://mastodon/bbb', last_follow=json_dumps({ 'actor': { 'publicInbox': 'https://public/inbox', 'inbox': 'https://unused', } })) Follower.get_or_create('orig', 'https://mastodon/ccc', last_follow=json_dumps({ 'actor': { 'endpoints': { 'sharedInbox': 'https://shared/inbox', }, } })) Follower.get_or_create('orig', 'https://mastodon/ddd', last_follow=json_dumps( {'actor': { 'inbox': 'https://inbox', }})) Follower.get_or_create('orig', 'https://mastodon/eee', status='inactive', last_follow=json_dumps( {'actor': { 'inbox': 'https://unused/2', }})) Follower.get_or_create( 'orig', 'https://mastodon/fff', last_follow=json_dumps({ 'actor': { # dupe of eee; should be de-duped 'inbox': 'https://inbox', } })) got = self.client.post('/webmention', data={ 'source': 'http://orig/post', 'target': 'https://fed.brid.gy/', }) self.assertEqual(200, got.status_code) mock_get.assert_has_calls((self.req('http://orig/post'), )) inboxes = ('https://inbox', 'https://public/inbox', 'https://shared/inbox') self.assertEqual(len(inboxes), len(mock_post.call_args_list)) for call, inbox in zip(mock_post.call_args_list, inboxes): self.assertEqual((inbox, ), call[0]) self.assertEqual(self.create_as2, json_loads(call[1]['data'])) for inbox in inboxes: resp = Response.get_by_id('http://orig/post %s' % inbox) self.assertEqual('out', resp.direction, inbox) self.assertEqual('activitypub', resp.protocol, inbox) self.assertEqual('complete', resp.status, inbox) self.assertEqual(self.create_mf2, json_loads(resp.source_mf2), inbox)