2017-08-13 07:12:16 +00:00
|
|
|
# coding=utf-8
|
2023-02-01 21:19:41 +00:00
|
|
|
"""Unit tests for activitypub.py."""
|
2023-02-15 18:57:11 +00:00
|
|
|
from base64 import b64encode
|
2017-08-23 15:14:51 +00:00
|
|
|
import copy
|
2023-01-20 15:02:55 +00:00
|
|
|
from datetime import datetime, timedelta
|
2023-02-15 18:57:11 +00:00
|
|
|
from hashlib import sha256
|
2023-02-16 04:10:17 +00:00
|
|
|
import logging
|
2023-05-24 04:30:57 +00:00
|
|
|
from unittest import skip
|
2022-02-09 14:52:41 +00:00
|
|
|
from unittest.mock import ANY, call, patch
|
2023-02-22 03:19:56 +00:00
|
|
|
import urllib.parse
|
2017-08-13 07:12:16 +00:00
|
|
|
|
2023-03-20 21:28:14 +00:00
|
|
|
from flask import g
|
2023-02-15 18:57:11 +00:00
|
|
|
from google.cloud import ndb
|
2023-02-24 03:17:26 +00:00
|
|
|
from granary import as2, microformats2
|
2023-02-16 06:05:14 +00:00
|
|
|
from httpsig import HeaderSigner
|
2017-08-23 15:14:51 +00:00
|
|
|
from oauth_dropins.webutil import util
|
2017-08-24 14:41:46 +00:00
|
|
|
from oauth_dropins.webutil.testutil import requests_response
|
2019-12-25 07:26:58 +00:00
|
|
|
from oauth_dropins.webutil.util import json_dumps, json_loads
|
2017-08-13 07:12:16 +00:00
|
|
|
import requests
|
2021-09-01 15:19:38 +00:00
|
|
|
from urllib3.exceptions import ReadTimeoutError
|
2023-03-08 21:10:41 +00:00
|
|
|
from werkzeug.exceptions import BadGateway
|
2017-08-13 07:12:16 +00:00
|
|
|
|
|
|
|
import activitypub
|
2023-03-08 21:10:41 +00:00
|
|
|
from activitypub import ActivityPub
|
2023-04-19 00:17:48 +00:00
|
|
|
from flask_app import app
|
2017-08-15 06:07:24 +00:00
|
|
|
import common
|
2023-03-08 21:10:41 +00:00
|
|
|
import models
|
2023-05-26 23:07:36 +00:00
|
|
|
from models import Follower, Object
|
2023-03-08 21:10:41 +00:00
|
|
|
import protocol
|
|
|
|
from protocol import Protocol
|
2023-05-27 00:40:29 +00:00
|
|
|
from web import Web
|
2023-05-26 23:07:36 +00:00
|
|
|
|
|
|
|
from .testutil import Fake, TestCase
|
2017-08-13 07:12:16 +00:00
|
|
|
|
2023-02-08 18:48:05 +00:00
|
|
|
ACTOR = {
|
|
|
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
2023-03-19 22:43:55 +00:00
|
|
|
'id': 'https://mas.to/users/swentel',
|
2023-02-08 18:48:05 +00:00
|
|
|
'type': 'Person',
|
2023-03-19 22:43:55 +00:00
|
|
|
'inbox': 'http://mas.to/inbox',
|
2023-02-14 16:25:41 +00:00
|
|
|
'name': 'Mrs. ☕ Foo',
|
2023-03-19 22:43:55 +00:00
|
|
|
'icon': {'type': 'Image', 'url': 'https://user.com/me.jpg'},
|
2023-02-08 18:48:05 +00:00
|
|
|
}
|
2018-10-15 15:09:36 +00:00
|
|
|
REPLY_OBJECT = {
|
|
|
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
|
|
'type': 'Note',
|
|
|
|
'content': 'A ☕ reply',
|
2023-03-19 22:43:55 +00:00
|
|
|
'id': 'http://mas.to/reply/id',
|
|
|
|
'url': 'http://mas.to/reply',
|
|
|
|
'inReplyTo': 'https://user.com/post',
|
2022-08-24 00:37:50 +00:00
|
|
|
'to': [as2.PUBLIC_AUDIENCE],
|
2018-10-15 15:09:36 +00:00
|
|
|
}
|
2018-10-17 14:00:31 +00:00
|
|
|
REPLY_OBJECT_WRAPPED = copy.deepcopy(REPLY_OBJECT)
|
2023-03-19 22:43:55 +00:00
|
|
|
REPLY_OBJECT_WRAPPED['inReplyTo'] = 'http://localhost/r/https://user.com/post'
|
2018-10-21 22:28:42 +00:00
|
|
|
REPLY = {
|
2018-10-15 15:09:36 +00:00
|
|
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
|
|
'type': 'Create',
|
2023-03-19 22:43:55 +00:00
|
|
|
'id': 'http://mas.to/reply/as2',
|
2018-10-15 15:09:36 +00:00
|
|
|
'object': REPLY_OBJECT,
|
|
|
|
}
|
2022-11-14 05:46:26 +00:00
|
|
|
NOTE_OBJECT = {
|
2019-04-16 14:59:29 +00:00
|
|
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
|
|
'type': 'Note',
|
2022-11-14 05:46:26 +00:00
|
|
|
'content': '☕ just a normal post',
|
2023-03-19 22:43:55 +00:00
|
|
|
'id': 'http://mas.to/note/id',
|
|
|
|
'url': 'http://mas.to/note',
|
2022-08-24 00:37:50 +00:00
|
|
|
'to': [as2.PUBLIC_AUDIENCE],
|
2019-04-16 14:59:29 +00:00
|
|
|
'cc': [
|
2023-03-19 22:43:55 +00:00
|
|
|
'https://mas.to/author/followers',
|
2019-04-16 14:59:29 +00:00
|
|
|
'https://masto.foo/@other',
|
|
|
|
'http://localhost/target', # redirect-wrapped
|
|
|
|
],
|
|
|
|
}
|
2022-11-14 05:46:26 +00:00
|
|
|
NOTE = {
|
|
|
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
|
|
'type': 'Create',
|
2023-03-19 22:43:55 +00:00
|
|
|
'id': 'http://mas.to/note/as2',
|
2022-11-17 02:53:49 +00:00
|
|
|
'actor': 'https://masto.foo/@author',
|
2022-11-14 05:46:26 +00:00
|
|
|
'object': NOTE_OBJECT,
|
|
|
|
}
|
|
|
|
MENTION_OBJECT = copy.deepcopy(NOTE_OBJECT)
|
2022-11-17 02:53:49 +00:00
|
|
|
MENTION_OBJECT.update({
|
2023-03-19 22:43:55 +00:00
|
|
|
'id': 'http://mas.to/mention/id',
|
|
|
|
'url': 'http://mas.to/mention',
|
2022-11-17 02:53:49 +00:00
|
|
|
'tag': [{
|
|
|
|
'type': 'Mention',
|
|
|
|
'href': 'https://masto.foo/@other',
|
|
|
|
'name': '@other@masto.foo',
|
|
|
|
}, {
|
|
|
|
'type': 'Mention',
|
2022-12-10 17:01:04 +00:00
|
|
|
'href': 'http://localhost/tar.get', # redirect-wrapped
|
|
|
|
'name': '@tar.get@tar.get',
|
2022-11-17 02:53:49 +00:00
|
|
|
}],
|
|
|
|
})
|
2019-04-16 14:59:29 +00:00
|
|
|
MENTION = {
|
|
|
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
|
|
'type': 'Create',
|
2023-03-19 22:43:55 +00:00
|
|
|
'id': 'http://mas.to/mention/as2',
|
2019-04-16 14:59:29 +00:00
|
|
|
'object': MENTION_OBJECT,
|
|
|
|
}
|
2018-10-17 14:49:04 +00:00
|
|
|
# based on example Mastodon like:
|
|
|
|
# https://github.com/snarfed/bridgy-fed/issues/4#issuecomment-334212362
|
|
|
|
# (reposts are very similar)
|
2018-10-21 22:28:42 +00:00
|
|
|
LIKE = {
|
2018-10-17 14:49:04 +00:00
|
|
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
2023-03-19 22:43:55 +00:00
|
|
|
'id': 'http://mas.to/like#ok',
|
2018-10-17 14:49:04 +00:00
|
|
|
'type': 'Like',
|
2023-03-19 22:43:55 +00:00
|
|
|
'object': 'https://user.com/post',
|
|
|
|
'actor': 'https://user.com/actor',
|
2018-10-17 14:49:04 +00:00
|
|
|
}
|
2018-10-21 22:28:42 +00:00
|
|
|
LIKE_WRAPPED = copy.deepcopy(LIKE)
|
2023-03-19 22:43:55 +00:00
|
|
|
LIKE_WRAPPED['object'] = 'http://localhost/r/https://user.com/post'
|
2018-10-23 14:52:30 +00:00
|
|
|
LIKE_WITH_ACTOR = copy.deepcopy(LIKE)
|
2023-02-08 18:48:05 +00:00
|
|
|
# TODO: use ACTOR instead
|
2018-10-23 14:52:30 +00:00
|
|
|
LIKE_WITH_ACTOR['actor'] = {
|
|
|
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
2023-03-19 22:43:55 +00:00
|
|
|
'id': 'https://user.com/actor',
|
2018-10-23 14:52:30 +00:00
|
|
|
'type': 'Person',
|
|
|
|
'name': 'Ms. Actor',
|
|
|
|
'preferredUsername': 'msactor',
|
2023-03-19 22:43:55 +00:00
|
|
|
'image': {'type': 'Image', 'url': 'https://user.com/pic.jpg'},
|
2018-10-23 14:52:30 +00:00
|
|
|
}
|
2018-10-21 22:28:42 +00:00
|
|
|
|
2023-03-19 23:21:44 +00:00
|
|
|
# repost, should be delivered to followers if object is a fediverse post,
|
|
|
|
# translated to webmention if object is an indieweb post
|
2023-02-08 18:48:05 +00:00
|
|
|
REPOST = {
|
|
|
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
2023-02-19 01:53:27 +00:00
|
|
|
'id': 'https://mas.to/users/alice/statuses/654/activity',
|
2023-02-08 18:48:05 +00:00
|
|
|
'type': 'Announce',
|
|
|
|
'actor': ACTOR['id'],
|
2023-02-12 03:58:07 +00:00
|
|
|
'object': NOTE_OBJECT['id'],
|
2023-02-08 18:48:05 +00:00
|
|
|
'published': '2023-02-08T17:44:16Z',
|
|
|
|
'to': ['https://www.w3.org/ns/activitystreams#Public'],
|
2022-11-17 02:53:49 +00:00
|
|
|
}
|
2023-02-12 03:58:07 +00:00
|
|
|
REPOST_FULL = {
|
|
|
|
**REPOST,
|
|
|
|
'actor': ACTOR,
|
|
|
|
'object': NOTE_OBJECT,
|
|
|
|
}
|
2023-02-08 18:48:05 +00:00
|
|
|
|
2018-10-23 14:52:30 +00:00
|
|
|
FOLLOW = {
|
2018-10-21 22:28:42 +00:00
|
|
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
2023-03-19 22:43:55 +00:00
|
|
|
'id': 'https://mas.to/6d1a',
|
2018-10-21 22:28:42 +00:00
|
|
|
'type': 'Follow',
|
2022-11-17 02:53:49 +00:00
|
|
|
'actor': ACTOR['id'],
|
2023-03-19 22:43:55 +00:00
|
|
|
'object': 'https://user.com/',
|
2018-10-21 22:28:42 +00:00
|
|
|
}
|
2018-10-23 14:52:30 +00:00
|
|
|
FOLLOW_WRAPPED = copy.deepcopy(FOLLOW)
|
2023-03-19 22:43:55 +00:00
|
|
|
FOLLOW_WRAPPED['object'] = 'http://localhost/user.com'
|
2018-10-23 14:52:30 +00:00
|
|
|
FOLLOW_WITH_ACTOR = copy.deepcopy(FOLLOW)
|
2022-11-17 02:53:49 +00:00
|
|
|
FOLLOW_WITH_ACTOR['actor'] = ACTOR
|
2018-10-23 14:52:30 +00:00
|
|
|
FOLLOW_WRAPPED_WITH_ACTOR = copy.deepcopy(FOLLOW_WRAPPED)
|
2022-11-17 02:53:49 +00:00
|
|
|
FOLLOW_WRAPPED_WITH_ACTOR['actor'] = ACTOR
|
2023-01-19 15:00:13 +00:00
|
|
|
FOLLOW_WITH_OBJECT = copy.deepcopy(FOLLOW)
|
|
|
|
FOLLOW_WITH_OBJECT['object'] = ACTOR
|
2018-10-23 14:52:30 +00:00
|
|
|
|
2018-10-21 22:28:42 +00:00
|
|
|
ACCEPT = {
|
|
|
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
|
|
'type': 'Accept',
|
2023-03-21 02:17:55 +00:00
|
|
|
'id': 'http://localhost/user/user.com/followers#accept-https://mas.to/6d1a',
|
2023-03-19 22:43:55 +00:00
|
|
|
'actor': 'http://localhost/user.com',
|
2018-10-21 22:28:42 +00:00
|
|
|
'object': {
|
|
|
|
'type': 'Follow',
|
2023-03-19 22:43:55 +00:00
|
|
|
'actor': 'https://mas.to/users/swentel',
|
|
|
|
'object': 'http://localhost/user.com',
|
2023-03-21 02:17:55 +00:00
|
|
|
},
|
2018-10-21 22:28:42 +00:00
|
|
|
}
|
2018-10-15 15:09:36 +00:00
|
|
|
|
2019-08-01 14:32:45 +00:00
|
|
|
UNDO_FOLLOW_WRAPPED = {
|
|
|
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
2023-03-19 22:43:55 +00:00
|
|
|
'id': 'https://mas.to/6d1b',
|
2019-08-01 14:32:45 +00:00
|
|
|
'type': 'Undo',
|
2023-03-19 22:43:55 +00:00
|
|
|
'actor': 'https://mas.to/users/swentel',
|
2019-08-01 14:32:45 +00:00
|
|
|
'object': FOLLOW_WRAPPED,
|
|
|
|
}
|
|
|
|
|
2020-03-01 04:28:53 +00:00
|
|
|
DELETE = {
|
|
|
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
2023-03-19 22:43:55 +00:00
|
|
|
'id': 'https://mas.to/users/swentel#delete',
|
2020-03-01 04:28:53 +00:00
|
|
|
'type': 'Delete',
|
2023-03-19 22:43:55 +00:00
|
|
|
'actor': 'https://mas.to/users/swentel',
|
|
|
|
'object': 'https://mas.to/users/swentel',
|
2020-03-01 04:28:53 +00:00
|
|
|
}
|
|
|
|
|
2023-01-26 03:44:48 +00:00
|
|
|
UPDATE_PERSON = {
|
|
|
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
|
|
'id': 'https://a/person#update',
|
|
|
|
'type': 'Update',
|
2023-03-19 22:43:55 +00:00
|
|
|
'actor': 'https://mas.to/users/swentel',
|
2023-01-26 03:44:48 +00:00
|
|
|
'object': {
|
|
|
|
'type': 'Person',
|
|
|
|
'id': 'https://a/person',
|
|
|
|
},
|
|
|
|
}
|
|
|
|
UPDATE_NOTE = {
|
|
|
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
|
|
'id': 'https://a/note#update',
|
|
|
|
'type': 'Update',
|
2023-03-19 22:43:55 +00:00
|
|
|
'actor': 'https://mas.to/users/swentel',
|
2023-01-26 03:44:48 +00:00
|
|
|
'object': {
|
|
|
|
'type': 'Note',
|
|
|
|
'id': 'https://a/note',
|
|
|
|
},
|
|
|
|
}
|
2023-02-19 01:53:27 +00:00
|
|
|
WEBMENTION_DISCOVERY = requests_response(
|
|
|
|
'<html><head><link rel="webmention" href="/webmention"></html>')
|
|
|
|
|
2023-03-08 21:10:41 +00:00
|
|
|
HTML = requests_response('<html></html>', headers={
|
|
|
|
'Content-Type': common.CONTENT_TYPE_HTML,
|
|
|
|
})
|
|
|
|
HTML_WITH_AS2 = requests_response("""\
|
|
|
|
<html><meta>
|
|
|
|
<link href='http://as2' rel='alternate' type='application/activity+json'>
|
|
|
|
</meta></html>
|
|
|
|
""", headers={
|
|
|
|
'Content-Type': common.CONTENT_TYPE_HTML,
|
|
|
|
})
|
|
|
|
AS2_OBJ = {'foo': ['bar']}
|
|
|
|
AS2 = requests_response(AS2_OBJ, headers={
|
|
|
|
'Content-Type': as2.CONTENT_TYPE,
|
|
|
|
})
|
|
|
|
NOT_ACCEPTABLE = requests_response(status=406)
|
|
|
|
|
2020-03-01 04:28:53 +00:00
|
|
|
|
2017-10-13 06:14:46 +00:00
|
|
|
@patch('requests.post')
|
|
|
|
@patch('requests.get')
|
2018-10-23 14:11:44 +00:00
|
|
|
@patch('requests.head')
|
2023-05-26 23:07:36 +00:00
|
|
|
class ActivityPubTest(TestCase):
|
2017-08-13 07:12:16 +00:00
|
|
|
|
2023-02-08 02:25:24 +00:00
|
|
|
def setUp(self):
|
|
|
|
super().setUp()
|
2023-05-26 23:07:36 +00:00
|
|
|
self.user = self.make_user('user.com',
|
|
|
|
has_hcard=True, actor_as2=ACTOR)
|
2023-05-23 06:09:36 +00:00
|
|
|
with self.request_context:
|
2023-02-26 13:34:15 +00:00
|
|
|
self.key_id_obj = Object(id='http://my/key/id', as2={
|
|
|
|
**ACTOR,
|
|
|
|
'publicKey': {
|
|
|
|
'id': 'http://my/key/id#unused',
|
|
|
|
'owner': 'http://own/er',
|
|
|
|
'publicKeyPem': self.user.public_pem().decode(),
|
|
|
|
},
|
2023-03-29 19:48:50 +00:00
|
|
|
})
|
|
|
|
self.key_id_obj.put()
|
2023-02-26 13:34:15 +00:00
|
|
|
|
2023-03-21 02:17:55 +00:00
|
|
|
def assert_object(self, id, **props):
|
|
|
|
return super().assert_object(id, delivered_protocol='webmention', **props)
|
|
|
|
|
2023-02-26 13:34:15 +00:00
|
|
|
def sign(self, path, body):
|
|
|
|
"""Constructs HTTP Signature, returns headers."""
|
|
|
|
digest = b64encode(sha256(body.encode()).digest()).decode()
|
|
|
|
headers = {
|
|
|
|
'Date': 'Sun, 02 Jan 2022 03:04:05 GMT',
|
|
|
|
'Host': 'localhost',
|
|
|
|
'Content-Type': as2.CONTENT_TYPE,
|
|
|
|
'Digest': f'SHA-256={digest}',
|
|
|
|
}
|
|
|
|
hs = HeaderSigner('http://my/key/id#unused', self.user.private_pem().decode(),
|
|
|
|
algorithm='rsa-sha256', sign_header='signature',
|
|
|
|
headers=('Date', 'Host', 'Digest', '(request-target)'))
|
|
|
|
return hs.sign(headers, method='POST', path=path)
|
|
|
|
|
|
|
|
def post(self, path, json=None):
|
|
|
|
"""Wrapper around self.client.post that adds signature."""
|
|
|
|
body = json_dumps(json)
|
|
|
|
return self.client.post(path, data=body, headers=self.sign(path, body))
|
2023-02-08 02:25:24 +00:00
|
|
|
|
2023-02-14 16:25:41 +00:00
|
|
|
def test_actor(self, *_):
|
2023-03-19 22:43:55 +00:00
|
|
|
got = self.client.get('/user.com')
|
2021-07-10 15:07:40 +00:00
|
|
|
self.assertEqual(200, got.status_code)
|
2019-12-26 06:20:57 +00:00
|
|
|
type = got.headers['Content-Type']
|
2023-01-07 05:01:33 +00:00
|
|
|
self.assertTrue(type.startswith(as2.CONTENT_TYPE), type)
|
2019-12-26 06:20:57 +00:00
|
|
|
self.assertEqual({
|
2021-03-23 06:55:14 +00:00
|
|
|
'@context': [
|
|
|
|
'https://www.w3.org/ns/activitystreams',
|
|
|
|
'https://w3id.org/security/v1',
|
|
|
|
],
|
2017-09-28 14:25:21 +00:00
|
|
|
'type' : 'Person',
|
2017-10-13 06:16:32 +00:00
|
|
|
'name': 'Mrs. ☕ Foo',
|
2019-01-04 15:04:45 +00:00
|
|
|
'summary': '',
|
2023-03-19 22:43:55 +00:00
|
|
|
'preferredUsername': 'user.com',
|
|
|
|
'id': 'http://localhost/user.com',
|
|
|
|
'url': 'http://localhost/r/https://user.com/',
|
|
|
|
'icon': {'type': 'Image', 'url': 'https://user.com/me.jpg'},
|
|
|
|
'inbox': 'http://localhost/user.com/inbox',
|
|
|
|
'outbox': 'http://localhost/user.com/outbox',
|
|
|
|
'following': 'http://localhost/user.com/following',
|
|
|
|
'followers': 'http://localhost/user.com/followers',
|
2022-11-17 15:42:41 +00:00
|
|
|
'endpoints': {
|
|
|
|
'sharedInbox': 'http://localhost/inbox',
|
|
|
|
},
|
2017-10-01 14:09:22 +00:00
|
|
|
'publicKey': {
|
2023-03-19 22:43:55 +00:00
|
|
|
'id': 'http://localhost/user.com',
|
|
|
|
'owner': 'http://localhost/user.com',
|
2023-02-15 18:57:11 +00:00
|
|
|
'publicKeyPem': self.user.public_pem().decode(),
|
2017-10-01 14:09:22 +00:00
|
|
|
},
|
2021-07-10 15:07:40 +00:00
|
|
|
}, got.json)
|
2017-08-13 21:49:35 +00:00
|
|
|
|
2020-03-02 15:50:37 +00:00
|
|
|
def test_actor_blocked_tld(self, _, __, ___):
|
2021-08-18 14:59:52 +00:00
|
|
|
got = self.client.get('/foo.json')
|
2021-07-10 15:07:40 +00:00
|
|
|
self.assertEqual(404, got.status_code)
|
2020-03-02 15:50:37 +00:00
|
|
|
|
2023-02-08 02:25:24 +00:00
|
|
|
def test_actor_no_user(self, *mocks):
|
|
|
|
got = self.client.get('/nope.com')
|
|
|
|
self.assertEqual(404, got.status_code)
|
|
|
|
|
|
|
|
def test_individual_inbox_no_user(self, *mocks):
|
2023-02-26 13:34:15 +00:00
|
|
|
got = self.post('/nope.com/inbox', json=REPLY)
|
2023-02-08 02:25:24 +00:00
|
|
|
self.assertEqual(404, got.status_code)
|
|
|
|
|
2023-02-12 06:53:50 +00:00
|
|
|
def test_inbox_activity_without_id(self, *_):
|
|
|
|
note = copy.deepcopy(NOTE)
|
|
|
|
del note['id']
|
2023-02-26 13:34:15 +00:00
|
|
|
resp = self.post('/inbox', json=note)
|
2023-02-12 06:53:50 +00:00
|
|
|
self.assertEqual(400, resp.status_code)
|
|
|
|
|
2018-10-23 14:11:44 +00:00
|
|
|
def test_inbox_reply_object(self, *mocks):
|
2023-01-30 01:30:11 +00:00
|
|
|
self._test_inbox_reply(REPLY_OBJECT,
|
2023-02-01 20:22:04 +00:00
|
|
|
{'as2': REPLY_OBJECT,
|
|
|
|
'type': 'comment',
|
|
|
|
'labels': ['notification']},
|
2023-01-30 01:30:11 +00:00
|
|
|
*mocks)
|
2018-10-15 15:09:36 +00:00
|
|
|
|
2018-10-23 14:11:44 +00:00
|
|
|
def test_inbox_reply_object_wrapped(self, *mocks):
|
2023-01-30 01:30:11 +00:00
|
|
|
self._test_inbox_reply(REPLY_OBJECT_WRAPPED,
|
|
|
|
{'as2': REPLY_OBJECT,
|
2023-02-01 20:22:04 +00:00
|
|
|
'type': 'comment',
|
|
|
|
'labels': ['notification']},
|
2023-01-30 01:30:11 +00:00
|
|
|
*mocks)
|
2018-10-17 14:00:31 +00:00
|
|
|
|
2018-10-23 14:11:44 +00:00
|
|
|
def test_inbox_reply_create_activity(self, *mocks):
|
2023-01-30 01:30:11 +00:00
|
|
|
self._test_inbox_reply(REPLY,
|
|
|
|
{'as2': REPLY,
|
|
|
|
'type': 'post',
|
2023-02-01 20:22:04 +00:00
|
|
|
'object_ids': [REPLY_OBJECT['id']],
|
|
|
|
'labels': ['notification', 'activity'],
|
|
|
|
},
|
2023-01-30 01:30:11 +00:00
|
|
|
*mocks)
|
2023-03-03 23:12:51 +00:00
|
|
|
self.assert_object(REPLY_OBJECT['id'],
|
|
|
|
source_protocol='activitypub',
|
|
|
|
as2=REPLY_OBJECT,
|
|
|
|
type='comment')
|
2018-10-15 15:09:36 +00:00
|
|
|
|
2023-01-30 01:30:11 +00:00
|
|
|
def _test_inbox_reply(self, reply, expected_props, mock_head, mock_get, mock_post):
|
2023-03-19 22:43:55 +00:00
|
|
|
mock_head.return_value = requests_response(url='https://user.com/post')
|
2023-03-10 03:56:04 +00:00
|
|
|
mock_get.return_value = WEBMENTION_DISCOVERY
|
2017-08-24 14:41:46 +00:00
|
|
|
mock_post.return_value = requests_response()
|
2017-08-13 21:49:35 +00:00
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
got = self.post('/user.com/inbox', json=reply)
|
2021-07-10 15:07:40 +00:00
|
|
|
self.assertEqual(200, got.status_code, got.get_data(as_text=True))
|
2023-03-19 22:43:55 +00:00
|
|
|
self.assert_req(mock_get, 'https://user.com/post')
|
2023-05-24 23:31:42 +00:00
|
|
|
convert_id = reply['id'].replace('://', ':/')
|
2022-03-17 04:11:09 +00:00
|
|
|
self.assert_req(
|
|
|
|
mock_post,
|
2023-03-19 22:43:55 +00:00
|
|
|
'https://user.com/webmention',
|
2022-03-17 04:11:09 +00:00
|
|
|
headers={'Accept': '*/*'},
|
|
|
|
allow_redirects=False,
|
2017-08-13 21:49:35 +00:00
|
|
|
data={
|
2023-05-24 23:31:42 +00:00
|
|
|
'source': f'http://localhost/convert/activitypub/webmention/{convert_id}',
|
2023-03-19 22:43:55 +00:00
|
|
|
'target': 'https://user.com/post',
|
2017-08-13 21:49:35 +00:00
|
|
|
},
|
2022-03-17 04:11:09 +00:00
|
|
|
)
|
2017-10-04 14:07:22 +00:00
|
|
|
|
2023-02-19 01:53:27 +00:00
|
|
|
self.assert_object(reply['id'],
|
2023-03-19 22:43:55 +00:00
|
|
|
domains=['user.com'],
|
2023-01-29 22:13:58 +00:00
|
|
|
source_protocol='activitypub',
|
|
|
|
status='complete',
|
2023-03-19 22:43:55 +00:00
|
|
|
delivered=['https://user.com/post'],
|
2023-01-30 01:30:11 +00:00
|
|
|
**expected_props)
|
2017-10-10 00:29:50 +00:00
|
|
|
|
2022-11-14 15:07:33 +00:00
|
|
|
def test_inbox_reply_to_self_domain(self, mock_head, mock_get, mock_post):
|
2023-03-19 22:43:55 +00:00
|
|
|
self._test_inbox_ignore_reply_to('http://localhost/mas.to',
|
2022-11-14 15:07:33 +00:00
|
|
|
mock_head, mock_get, mock_post)
|
|
|
|
|
|
|
|
def test_inbox_reply_to_in_blocklist(self, *mocks):
|
2022-11-17 02:53:49 +00:00
|
|
|
self._test_inbox_ignore_reply_to('https://twitter.com/foo', *mocks)
|
2022-11-14 15:07:33 +00:00
|
|
|
|
2022-11-17 02:53:49 +00:00
|
|
|
def _test_inbox_ignore_reply_to(self, reply_to, mock_head, mock_get, mock_post):
|
2019-04-16 14:42:10 +00:00
|
|
|
reply = copy.deepcopy(REPLY_OBJECT)
|
2022-11-14 15:07:33 +00:00
|
|
|
reply['inReplyTo'] = reply_to
|
2019-04-16 14:42:10 +00:00
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
mock_head.return_value = requests_response(url='http://mas.to/')
|
2019-04-16 14:42:10 +00:00
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
got = self.post('/user.com/inbox', json=reply)
|
2022-11-17 02:53:49 +00:00
|
|
|
self.assertEqual(200, got.status_code, got.get_data(as_text=True))
|
2019-04-16 14:42:10 +00:00
|
|
|
|
|
|
|
mock_get.assert_not_called()
|
|
|
|
mock_post.assert_not_called()
|
|
|
|
|
2023-02-08 02:25:24 +00:00
|
|
|
def test_individual_inbox_create_obj(self, *mocks):
|
2023-03-19 22:43:55 +00:00
|
|
|
self._test_inbox_create_obj('/user.com/inbox', *mocks)
|
2023-02-07 04:08:19 +00:00
|
|
|
|
|
|
|
def test_shared_inbox_create_obj(self, *mocks):
|
|
|
|
self._test_inbox_create_obj('/inbox', *mocks)
|
|
|
|
|
|
|
|
def _test_inbox_create_obj(self, path, mock_head, mock_get, mock_post):
|
2023-03-19 22:43:55 +00:00
|
|
|
Follower.get_or_create(NOTE['actor'], 'user.com')
|
2022-11-17 02:53:49 +00:00
|
|
|
Follower.get_or_create('http://other/actor', 'bar.com')
|
2023-03-03 17:24:59 +00:00
|
|
|
Follower.get_or_create(NOTE['actor'], 'baz.com')
|
2023-03-04 20:12:45 +00:00
|
|
|
Follower.get_or_create(NOTE['actor'], 'baj.com', status='inactive')
|
2022-11-17 02:53:49 +00:00
|
|
|
|
|
|
|
mock_head.return_value = requests_response(url='http://target')
|
2023-01-07 17:18:11 +00:00
|
|
|
mock_get.return_value = self.as2_resp(ACTOR) # source actor
|
2022-11-17 02:53:49 +00:00
|
|
|
mock_post.return_value = requests_response()
|
|
|
|
|
2023-02-26 13:34:15 +00:00
|
|
|
got = self.post(path, json=NOTE)
|
2023-02-09 04:22:16 +00:00
|
|
|
self.assertEqual(200, got.status_code, got.get_data(as_text=True))
|
|
|
|
expected_as2 = common.redirect_unwrap({
|
|
|
|
**NOTE,
|
|
|
|
'actor': ACTOR,
|
|
|
|
})
|
2023-01-29 22:13:58 +00:00
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
self.assert_object('http://mas.to/note/as2',
|
2023-01-29 22:13:58 +00:00
|
|
|
source_protocol='activitypub',
|
|
|
|
as2=expected_as2,
|
2023-03-19 22:43:55 +00:00
|
|
|
domains=['user.com', 'baz.com'],
|
2023-01-30 01:30:11 +00:00
|
|
|
type='post',
|
2023-02-20 16:28:16 +00:00
|
|
|
labels=['activity', 'feed'],
|
2023-01-30 01:30:11 +00:00
|
|
|
object_ids=[NOTE_OBJECT['id']])
|
2023-03-03 23:12:51 +00:00
|
|
|
self.assert_object(NOTE_OBJECT['id'],
|
|
|
|
source_protocol='activitypub',
|
|
|
|
as2=NOTE_OBJECT,
|
|
|
|
type='note')
|
2022-11-17 02:53:49 +00:00
|
|
|
|
2023-03-19 23:21:44 +00:00
|
|
|
def test_repost_of_indieweb(self, mock_head, mock_get, mock_post):
|
2023-03-19 22:43:55 +00:00
|
|
|
mock_head.return_value = requests_response(url='https://user.com/orig')
|
2023-03-10 03:56:04 +00:00
|
|
|
mock_get.return_value = WEBMENTION_DISCOVERY
|
2023-02-22 03:19:56 +00:00
|
|
|
mock_post.return_value = requests_response() # webmention
|
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
orig_url = 'https://user.com/orig'
|
2023-02-22 03:19:56 +00:00
|
|
|
note = {
|
|
|
|
**NOTE_OBJECT,
|
2023-03-19 23:21:44 +00:00
|
|
|
'id': 'https://user.com/orig',
|
2023-02-22 03:19:56 +00:00
|
|
|
}
|
2023-03-19 23:21:44 +00:00
|
|
|
del note['url']
|
2023-05-23 06:09:36 +00:00
|
|
|
with self.request_context:
|
2023-03-19 23:21:44 +00:00
|
|
|
Object(id=orig_url, mf2=microformats2.object_to_json(as2.to_as1(note))).put()
|
2023-02-22 03:19:56 +00:00
|
|
|
|
2023-03-19 23:21:44 +00:00
|
|
|
repost = copy.deepcopy(REPOST_FULL)
|
|
|
|
repost['object'] = f'http://localhost/r/{orig_url}'
|
2023-03-19 22:43:55 +00:00
|
|
|
got = self.post('/user.com/inbox', json=repost)
|
2023-02-22 03:19:56 +00:00
|
|
|
self.assertEqual(200, got.status_code, got.get_data(as_text=True))
|
|
|
|
|
2023-05-24 23:31:42 +00:00
|
|
|
convert_id = REPOST['id'].replace('://', ':/')
|
2023-02-22 03:19:56 +00:00
|
|
|
self.assert_req(
|
|
|
|
mock_post,
|
2023-03-19 22:43:55 +00:00
|
|
|
'https://user.com/webmention',
|
2023-02-22 03:19:56 +00:00
|
|
|
headers={'Accept': '*/*'},
|
|
|
|
allow_redirects=False,
|
|
|
|
data={
|
2023-05-24 23:31:42 +00:00
|
|
|
'source': f'http://localhost/convert/activitypub/webmention/{convert_id}',
|
2023-02-22 03:19:56 +00:00
|
|
|
'target': orig_url,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
repost['object'] = note
|
2023-03-19 23:21:44 +00:00
|
|
|
del repost['object']['to']
|
|
|
|
del repost['object']['cc']
|
2023-02-22 03:19:56 +00:00
|
|
|
self.assert_object(REPOST_FULL['id'],
|
|
|
|
source_protocol='activitypub',
|
|
|
|
status='complete',
|
|
|
|
as2=repost,
|
2023-03-19 22:43:55 +00:00
|
|
|
domains=['user.com'],
|
|
|
|
delivered=['https://user.com/orig'],
|
2023-02-22 03:19:56 +00:00
|
|
|
type='share',
|
2023-02-24 03:17:26 +00:00
|
|
|
labels=['activity', 'feed', 'notification'],
|
2023-03-19 23:21:44 +00:00
|
|
|
object_ids=['https://user.com/orig'])
|
2023-02-22 03:19:56 +00:00
|
|
|
|
2023-03-19 23:21:44 +00:00
|
|
|
def test_shared_inbox_repost_of_fediverse(self, mock_head, mock_get, mock_post):
|
2023-03-19 22:43:55 +00:00
|
|
|
Follower.get_or_create(ACTOR['id'], 'user.com')
|
2023-02-08 18:48:05 +00:00
|
|
|
Follower.get_or_create(ACTOR['id'], 'baz.com')
|
2023-03-04 20:12:45 +00:00
|
|
|
Follower.get_or_create(ACTOR['id'], 'baj.com', status='inactive')
|
2023-02-08 18:48:05 +00:00
|
|
|
|
|
|
|
mock_head.return_value = requests_response(url='http://target')
|
2023-02-12 03:58:07 +00:00
|
|
|
mock_get.side_effect = [
|
|
|
|
self.as2_resp(ACTOR), # source actor
|
|
|
|
self.as2_resp(NOTE_OBJECT), # object of repost
|
2023-03-19 23:21:44 +00:00
|
|
|
HTML, # no webmention endpoint
|
2023-02-12 03:58:07 +00:00
|
|
|
]
|
2023-02-08 18:48:05 +00:00
|
|
|
|
2023-02-26 13:34:15 +00:00
|
|
|
got = self.post('/inbox', json=REPOST)
|
2023-02-08 18:48:05 +00:00
|
|
|
self.assertEqual(200, got.status_code, got.get_data(as_text=True))
|
|
|
|
|
2023-03-19 23:21:44 +00:00
|
|
|
mock_post.assert_not_called() # no webmention
|
|
|
|
|
2023-02-08 18:48:05 +00:00
|
|
|
self.assert_object(REPOST['id'],
|
|
|
|
source_protocol='activitypub',
|
2023-03-19 22:43:55 +00:00
|
|
|
status='ignored',
|
2023-02-12 03:58:07 +00:00
|
|
|
as2=REPOST_FULL,
|
2023-03-19 22:43:55 +00:00
|
|
|
domains=['user.com', 'baz.com'],
|
2023-02-08 18:48:05 +00:00
|
|
|
type='share',
|
2023-03-19 22:43:55 +00:00
|
|
|
labels=['activity', 'feed'],
|
2023-03-19 23:21:44 +00:00
|
|
|
object_ids=[REPOST['object']])
|
|
|
|
|
|
|
|
def test_inbox_no_user(self, mock_head, mock_get, mock_post):
|
|
|
|
mock_get.side_effect = [
|
|
|
|
# source actor
|
|
|
|
self.as2_resp(LIKE_WITH_ACTOR['actor']),
|
|
|
|
# target post webmention discovery
|
|
|
|
HTML,
|
|
|
|
]
|
|
|
|
|
|
|
|
got = self.post('/inbox', json={**LIKE, 'object': 'http://nope.com/post'})
|
|
|
|
self.assertEqual(200, got.status_code)
|
|
|
|
|
|
|
|
self.assert_object('http://mas.to/like#ok',
|
|
|
|
domains=['nope.com'],
|
|
|
|
source_protocol='activitypub',
|
|
|
|
status='complete',
|
|
|
|
as2={**LIKE_WITH_ACTOR, 'object': 'http://nope.com/post'},
|
|
|
|
type='like',
|
|
|
|
labels=['activity'],
|
|
|
|
object_ids=['http://nope.com/post'])
|
2023-02-08 18:48:05 +00:00
|
|
|
|
2022-08-24 00:37:50 +00:00
|
|
|
def test_inbox_not_public(self, mock_head, mock_get, mock_post):
|
2023-03-19 22:43:55 +00:00
|
|
|
Follower.get_or_create(ACTOR['id'], 'user.com')
|
2022-08-24 00:37:50 +00:00
|
|
|
|
|
|
|
mock_head.return_value = requests_response(url='http://target')
|
2023-01-07 17:18:11 +00:00
|
|
|
mock_get.return_value = self.as2_resp(ACTOR) # source actor
|
2022-08-24 00:37:50 +00:00
|
|
|
|
|
|
|
not_public = copy.deepcopy(NOTE)
|
|
|
|
del not_public['object']['to']
|
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
got = self.post('/user.com/inbox', json=not_public)
|
2023-02-09 04:22:16 +00:00
|
|
|
self.assertEqual(200, got.status_code, got.get_data(as_text=True))
|
2023-02-12 06:53:50 +00:00
|
|
|
|
2023-03-08 21:10:41 +00:00
|
|
|
self.assertIsNone(Object.get_by_id(not_public['id']))
|
2023-02-12 06:53:50 +00:00
|
|
|
self.assertIsNone(Object.get_by_id(not_public['object']['id']))
|
2022-08-24 00:37:50 +00:00
|
|
|
|
2019-04-16 14:59:29 +00:00
|
|
|
def test_inbox_mention_object(self, *mocks):
|
2023-01-30 01:30:11 +00:00
|
|
|
self._test_inbox_mention(
|
|
|
|
MENTION_OBJECT,
|
2023-02-01 20:22:04 +00:00
|
|
|
{
|
|
|
|
'type': 'note', # not mention (?)
|
|
|
|
'labels': ['notification'],
|
|
|
|
},
|
2023-01-30 01:30:11 +00:00
|
|
|
*mocks,
|
|
|
|
)
|
2019-04-16 14:59:29 +00:00
|
|
|
|
|
|
|
def test_inbox_mention_create_activity(self, *mocks):
|
2023-01-30 01:30:11 +00:00
|
|
|
self._test_inbox_mention(
|
|
|
|
MENTION,
|
|
|
|
{
|
|
|
|
'type': 'post', # not mention (?)
|
|
|
|
'object_ids': [MENTION_OBJECT['id']],
|
2023-02-01 20:22:04 +00:00
|
|
|
'labels': ['notification', 'activity'],
|
2023-01-30 01:30:11 +00:00
|
|
|
},
|
|
|
|
*mocks,
|
|
|
|
)
|
2019-04-16 14:59:29 +00:00
|
|
|
|
2023-03-03 23:12:51 +00:00
|
|
|
# redirect unwrap
|
|
|
|
expected_as2 = copy.deepcopy(MENTION_OBJECT)
|
|
|
|
expected_as2['tag'][1]['href'] = 'https://tar.get/'
|
|
|
|
self.assert_object(MENTION_OBJECT['id'],
|
|
|
|
source_protocol='activitypub',
|
|
|
|
as2=expected_as2,
|
|
|
|
type='note')
|
|
|
|
|
2023-01-30 01:30:11 +00:00
|
|
|
def _test_inbox_mention(self, mention, expected_props, mock_head, mock_get, mock_post):
|
2023-03-08 21:10:41 +00:00
|
|
|
mock_get.side_effect = [
|
|
|
|
WEBMENTION_DISCOVERY,
|
|
|
|
HTML,
|
|
|
|
]
|
2019-04-16 14:59:29 +00:00
|
|
|
mock_post.return_value = requests_response()
|
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
got = self.post('/user.com/inbox', json=mention)
|
2023-02-09 04:22:16 +00:00
|
|
|
self.assertEqual(200, got.status_code, got.get_data(as_text=True))
|
2023-02-20 16:28:16 +00:00
|
|
|
self.assert_req(mock_get, 'https://tar.get/')
|
2023-05-24 23:31:42 +00:00
|
|
|
convert_id = mention['id'].replace('://', ':/')
|
2023-02-09 04:22:16 +00:00
|
|
|
self.assert_req(
|
|
|
|
mock_post,
|
2023-02-20 16:28:16 +00:00
|
|
|
'https://tar.get/webmention',
|
2023-02-09 04:22:16 +00:00
|
|
|
headers={'Accept': '*/*'},
|
|
|
|
allow_redirects=False,
|
|
|
|
data={
|
2023-05-24 23:31:42 +00:00
|
|
|
'source': f'http://localhost/convert/activitypub/webmention/{convert_id}',
|
2023-02-20 16:28:16 +00:00
|
|
|
'target': 'https://tar.get/',
|
2023-02-09 04:22:16 +00:00
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
expected_as2 = common.redirect_unwrap(mention)
|
2023-02-19 01:53:27 +00:00
|
|
|
self.assert_object(mention['id'],
|
2023-03-08 21:10:41 +00:00
|
|
|
domains=['tar.get', 'masto.foo'],
|
2023-02-09 04:22:16 +00:00
|
|
|
source_protocol='activitypub',
|
|
|
|
status='complete',
|
|
|
|
as2=expected_as2,
|
2023-02-20 16:28:16 +00:00
|
|
|
delivered=['https://tar.get/'],
|
2023-02-09 04:22:16 +00:00
|
|
|
**expected_props)
|
2019-04-16 14:59:29 +00:00
|
|
|
|
2018-10-23 14:11:44 +00:00
|
|
|
def test_inbox_like(self, mock_head, mock_get, mock_post):
|
2023-03-19 22:43:55 +00:00
|
|
|
mock_head.return_value = requests_response(url='https://user.com/post')
|
2017-10-13 06:14:46 +00:00
|
|
|
mock_get.side_effect = [
|
|
|
|
# source actor
|
2023-01-07 17:18:11 +00:00
|
|
|
self.as2_resp(LIKE_WITH_ACTOR['actor']),
|
2023-02-19 01:53:27 +00:00
|
|
|
WEBMENTION_DISCOVERY,
|
2017-10-13 06:14:46 +00:00
|
|
|
]
|
2017-10-11 05:42:19 +00:00
|
|
|
mock_post.return_value = requests_response()
|
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
got = self.post('/user.com/inbox', json=LIKE)
|
2021-07-10 15:07:40 +00:00
|
|
|
self.assertEqual(200, got.status_code)
|
2017-10-13 06:14:46 +00:00
|
|
|
|
2022-11-24 16:20:04 +00:00
|
|
|
mock_get.assert_has_calls((
|
2023-03-19 22:43:55 +00:00
|
|
|
self.as2_req('https://user.com/actor'),
|
|
|
|
self.req('https://user.com/post'),
|
2022-11-24 16:20:04 +00:00
|
|
|
)),
|
2017-10-10 00:29:50 +00:00
|
|
|
|
2017-10-11 05:42:19 +00:00
|
|
|
args, kwargs = mock_post.call_args
|
2023-03-19 22:43:55 +00:00
|
|
|
self.assertEqual(('https://user.com/webmention',), args)
|
2019-12-26 06:20:57 +00:00
|
|
|
self.assertEqual({
|
2023-05-24 23:31:42 +00:00
|
|
|
'source': 'http://localhost/convert/activitypub/webmention/http:/mas.to/like^^ok',
|
2023-03-19 22:43:55 +00:00
|
|
|
'target': 'https://user.com/post',
|
2017-10-11 05:42:19 +00:00
|
|
|
}, kwargs['data'])
|
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
self.assert_object('http://mas.to/like#ok',
|
|
|
|
domains=['user.com'],
|
2023-01-29 22:13:58 +00:00
|
|
|
source_protocol='activitypub',
|
|
|
|
status='complete',
|
|
|
|
as2=LIKE_WITH_ACTOR,
|
2023-03-19 22:43:55 +00:00
|
|
|
delivered=['https://user.com/post'],
|
2023-01-30 01:30:11 +00:00
|
|
|
type='like',
|
2023-02-01 20:22:04 +00:00
|
|
|
labels=['notification', 'activity'],
|
2023-01-30 01:30:11 +00:00
|
|
|
object_ids=[LIKE['object']])
|
2017-10-17 05:21:13 +00:00
|
|
|
|
2023-02-12 06:23:01 +00:00
|
|
|
def test_inbox_follow_accept_with_id(self, *mocks):
|
|
|
|
self._test_inbox_follow_accept(FOLLOW_WRAPPED, ACCEPT, *mocks)
|
2023-01-09 01:28:12 +00:00
|
|
|
|
2023-03-03 17:24:59 +00:00
|
|
|
follow = {
|
|
|
|
**FOLLOW_WITH_ACTOR,
|
2023-03-19 22:43:55 +00:00
|
|
|
'url': 'https://mas.to/users/swentel#followed-https://user.com/',
|
2023-03-03 17:24:59 +00:00
|
|
|
}
|
2023-03-19 22:43:55 +00:00
|
|
|
self.assert_object('https://mas.to/6d1a',
|
|
|
|
domains=['user.com'],
|
2023-01-29 22:13:58 +00:00
|
|
|
source_protocol='activitypub',
|
|
|
|
status='complete',
|
|
|
|
as2=follow,
|
2023-03-19 22:43:55 +00:00
|
|
|
delivered=['https://user.com/'],
|
2023-01-30 01:30:11 +00:00
|
|
|
type='follow',
|
2023-02-01 20:22:04 +00:00
|
|
|
labels=['notification', 'activity'],
|
2023-01-30 01:30:11 +00:00
|
|
|
object_ids=[FOLLOW['object']])
|
2023-01-09 01:28:12 +00:00
|
|
|
|
|
|
|
follower = Follower.query().get()
|
2023-03-03 17:24:59 +00:00
|
|
|
self.assertEqual(follow, follower.last_follow)
|
2023-01-09 01:28:12 +00:00
|
|
|
|
2023-02-12 06:23:01 +00:00
|
|
|
def test_inbox_follow_accept_with_object(self, *mocks):
|
2023-01-09 01:28:12 +00:00
|
|
|
wrapped_user = {
|
|
|
|
'id': FOLLOW_WRAPPED['object'],
|
|
|
|
'url': FOLLOW_WRAPPED['object'],
|
|
|
|
}
|
|
|
|
unwrapped_user = {
|
|
|
|
'id': FOLLOW['object'],
|
|
|
|
'url': FOLLOW['object'],
|
|
|
|
}
|
|
|
|
|
2023-02-19 01:53:27 +00:00
|
|
|
follow = {
|
2023-03-03 17:24:59 +00:00
|
|
|
**FOLLOW,
|
|
|
|
'object': unwrapped_user,
|
2023-02-19 01:53:27 +00:00
|
|
|
}
|
2023-01-09 01:28:12 +00:00
|
|
|
|
2023-03-03 17:24:59 +00:00
|
|
|
self._test_inbox_follow_accept(follow, ACCEPT, *mocks)
|
2023-01-09 01:28:12 +00:00
|
|
|
|
|
|
|
follower = Follower.query().get()
|
2023-01-12 20:28:34 +00:00
|
|
|
follow.update({
|
2023-03-03 17:24:59 +00:00
|
|
|
'actor': ACTOR,
|
2023-03-19 22:43:55 +00:00
|
|
|
'url': 'https://mas.to/users/swentel#followed-https://user.com/',
|
2023-01-12 20:28:34 +00:00
|
|
|
})
|
2023-03-03 17:24:59 +00:00
|
|
|
self.assertEqual(follow, follower.last_follow)
|
2023-03-19 22:43:55 +00:00
|
|
|
self.assert_object('https://mas.to/6d1a',
|
|
|
|
domains=['user.com'],
|
2023-01-29 22:13:58 +00:00
|
|
|
source_protocol='activitypub',
|
|
|
|
status='complete',
|
|
|
|
as2=follow,
|
2023-03-19 22:43:55 +00:00
|
|
|
delivered=['https://user.com/'],
|
2023-01-30 01:30:11 +00:00
|
|
|
type='follow',
|
2023-02-01 20:22:04 +00:00
|
|
|
labels=['notification', 'activity'],
|
2023-01-30 01:30:11 +00:00
|
|
|
object_ids=[FOLLOW['object']])
|
2023-01-12 20:28:34 +00:00
|
|
|
|
2023-01-09 01:28:12 +00:00
|
|
|
def _test_inbox_follow_accept(self, follow_as2, accept_as2,
|
|
|
|
mock_head, mock_get, mock_post):
|
2023-03-19 22:43:55 +00:00
|
|
|
mock_head.return_value = requests_response(url='https://user.com/')
|
2018-10-21 22:28:42 +00:00
|
|
|
mock_get.side_effect = [
|
|
|
|
# source actor
|
2023-03-03 17:24:59 +00:00
|
|
|
self.as2_resp(ACTOR),
|
2023-02-19 01:53:27 +00:00
|
|
|
WEBMENTION_DISCOVERY,
|
2018-10-21 22:28:42 +00:00
|
|
|
]
|
|
|
|
mock_post.return_value = requests_response()
|
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
got = self.post('/user.com/inbox', json=follow_as2)
|
2021-07-10 15:07:40 +00:00
|
|
|
self.assertEqual(200, got.status_code)
|
2018-10-21 22:28:42 +00:00
|
|
|
|
2022-11-24 16:20:04 +00:00
|
|
|
mock_get.assert_has_calls((
|
|
|
|
self.as2_req(FOLLOW['actor']),
|
|
|
|
))
|
2018-10-21 22:28:42 +00:00
|
|
|
|
2018-10-23 14:11:44 +00:00
|
|
|
# check AP Accept
|
|
|
|
self.assertEqual(2, len(mock_post.call_args_list))
|
|
|
|
args, kwargs = mock_post.call_args_list[0]
|
2023-03-19 22:43:55 +00:00
|
|
|
self.assertEqual(('http://mas.to/inbox',), args)
|
2023-01-09 01:28:12 +00:00
|
|
|
self.assertEqual(accept_as2, json_loads(kwargs['data']))
|
2018-10-21 22:28:42 +00:00
|
|
|
|
2018-10-23 14:11:44 +00:00
|
|
|
# check webmention
|
|
|
|
args, kwargs = mock_post.call_args_list[1]
|
2023-03-19 22:43:55 +00:00
|
|
|
self.assertEqual(('https://user.com/webmention',), args)
|
2019-12-26 06:20:57 +00:00
|
|
|
self.assertEqual({
|
2023-05-24 23:31:42 +00:00
|
|
|
'source': 'http://localhost/convert/activitypub/webmention/https:/mas.to/6d1a',
|
2023-03-19 22:43:55 +00:00
|
|
|
'target': 'https://user.com/',
|
2018-10-23 14:11:44 +00:00
|
|
|
}, kwargs['data'])
|
|
|
|
|
2018-10-22 00:37:33 +00:00
|
|
|
# check that we stored a Follower object
|
2023-03-19 22:43:55 +00:00
|
|
|
follower = Follower.get_by_id(f'user.com {FOLLOW["actor"]}')
|
2019-08-01 14:32:45 +00:00
|
|
|
self.assertEqual('active', follower.status)
|
2018-10-22 00:37:33 +00:00
|
|
|
|
2022-12-06 22:09:44 +00:00
|
|
|
def test_inbox_follow_use_instead_strip_www(self, mock_head, mock_get, mock_post):
|
2023-03-19 22:43:55 +00:00
|
|
|
self.make_user('www.user.com', use_instead=self.user.key)
|
2022-12-06 22:09:44 +00:00
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
mock_head.return_value = requests_response(url='https://www.user.com/')
|
2022-12-06 22:09:44 +00:00
|
|
|
mock_get.side_effect = [
|
|
|
|
# source actor
|
2023-01-07 17:18:11 +00:00
|
|
|
self.as2_resp(ACTOR),
|
2022-12-06 22:09:44 +00:00
|
|
|
# target post webmention discovery
|
|
|
|
requests_response('<html></html>'),
|
|
|
|
]
|
|
|
|
mock_post.return_value = requests_response()
|
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
got = self.post('/user.com/inbox', json=FOLLOW_WRAPPED)
|
2022-12-06 22:09:44 +00:00
|
|
|
self.assertEqual(200, got.status_code)
|
|
|
|
|
|
|
|
# check that the Follower doesn't have www
|
2023-03-19 22:43:55 +00:00
|
|
|
follower = Follower.get_by_id(f'user.com {ACTOR["id"]}')
|
2022-12-06 22:09:44 +00:00
|
|
|
self.assertEqual('active', follower.status)
|
2023-03-03 17:24:59 +00:00
|
|
|
self.assertEqual({
|
|
|
|
**FOLLOW_WITH_ACTOR,
|
2023-03-19 22:43:55 +00:00
|
|
|
'url': 'https://mas.to/users/swentel#followed-https://user.com/',
|
2023-03-03 17:24:59 +00:00
|
|
|
}, follower.last_follow)
|
2022-12-06 22:09:44 +00:00
|
|
|
|
2019-08-01 14:32:45 +00:00
|
|
|
def test_inbox_undo_follow(self, mock_head, mock_get, mock_post):
|
2023-03-19 22:43:55 +00:00
|
|
|
mock_head.return_value = requests_response(url='https://user.com/')
|
2023-02-15 18:57:11 +00:00
|
|
|
mock_get.side_effect = [
|
|
|
|
self.as2_resp(ACTOR),
|
|
|
|
]
|
2019-08-01 14:32:45 +00:00
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
Follower.get_or_create('user.com', ACTOR['id'])
|
2019-08-01 14:32:45 +00:00
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
got = self.post('/user.com/inbox', json=UNDO_FOLLOW_WRAPPED)
|
2021-07-10 15:07:40 +00:00
|
|
|
self.assertEqual(200, got.status_code)
|
2019-08-01 14:32:45 +00:00
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
follower = Follower.get_by_id(f'user.com {FOLLOW["actor"]}')
|
2019-08-01 14:32:45 +00:00
|
|
|
self.assertEqual('inactive', follower.status)
|
|
|
|
|
2022-11-30 06:43:04 +00:00
|
|
|
def test_inbox_follow_inactive(self, mock_head, mock_get, mock_post):
|
2023-03-19 22:43:55 +00:00
|
|
|
Follower.get_or_create('user.com', ACTOR['id'], status='inactive')
|
2022-11-30 06:43:04 +00:00
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
mock_head.return_value = requests_response(url='https://user.com/')
|
2022-11-30 06:43:04 +00:00
|
|
|
mock_get.side_effect = [
|
|
|
|
# source actor
|
2023-01-07 17:18:11 +00:00
|
|
|
self.as2_resp(FOLLOW_WITH_ACTOR['actor']),
|
2023-02-19 01:53:27 +00:00
|
|
|
WEBMENTION_DISCOVERY,
|
2022-11-30 06:43:04 +00:00
|
|
|
]
|
|
|
|
mock_post.return_value = requests_response()
|
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
got = self.post('/user.com/inbox', json=FOLLOW_WRAPPED)
|
2022-11-30 06:43:04 +00:00
|
|
|
self.assertEqual(200, got.status_code)
|
|
|
|
|
|
|
|
# check that the Follower is now active
|
2023-03-19 22:43:55 +00:00
|
|
|
follower = Follower.get_by_id(f'user.com {FOLLOW["actor"]}')
|
2022-11-30 06:43:04 +00:00
|
|
|
self.assertEqual('active', follower.status)
|
|
|
|
|
2019-08-01 14:32:45 +00:00
|
|
|
def test_inbox_undo_follow_doesnt_exist(self, mock_head, mock_get, mock_post):
|
2023-03-19 22:43:55 +00:00
|
|
|
mock_head.return_value = requests_response(url='https://user.com/')
|
2023-02-15 18:57:11 +00:00
|
|
|
mock_get.side_effect = [
|
|
|
|
self.as2_resp(ACTOR),
|
|
|
|
]
|
2019-08-01 14:32:45 +00:00
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
got = self.post('/user.com/inbox', json=UNDO_FOLLOW_WRAPPED)
|
2021-07-10 15:07:40 +00:00
|
|
|
self.assertEqual(200, got.status_code)
|
2019-08-01 14:32:45 +00:00
|
|
|
|
|
|
|
def test_inbox_undo_follow_inactive(self, mock_head, mock_get, mock_post):
|
2023-03-19 22:43:55 +00:00
|
|
|
mock_head.return_value = requests_response(url='https://user.com/')
|
2023-02-15 18:57:11 +00:00
|
|
|
mock_get.side_effect = [
|
|
|
|
self.as2_resp(ACTOR),
|
|
|
|
]
|
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
Follower.get_or_create('user.com', ACTOR['id'], status='inactive')
|
2019-08-01 14:32:45 +00:00
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
got = self.post('/user.com/inbox', json=UNDO_FOLLOW_WRAPPED)
|
2021-07-10 15:07:40 +00:00
|
|
|
self.assertEqual(200, got.status_code)
|
2019-08-01 14:32:45 +00:00
|
|
|
|
2023-01-10 06:58:30 +00:00
|
|
|
def test_inbox_undo_follow_composite_object(self, mock_head, mock_get, mock_post):
|
2023-03-19 22:43:55 +00:00
|
|
|
mock_head.return_value = requests_response(url='https://user.com/')
|
2023-02-15 18:57:11 +00:00
|
|
|
mock_get.side_effect = [
|
|
|
|
self.as2_resp(ACTOR),
|
|
|
|
]
|
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
Follower.get_or_create('user.com', ACTOR['id'], status='inactive')
|
2023-01-10 06:58:30 +00:00
|
|
|
|
|
|
|
undo_follow = copy.deepcopy(UNDO_FOLLOW_WRAPPED)
|
|
|
|
undo_follow['object']['object'] = {'id': undo_follow['object']['object']}
|
2023-03-19 22:43:55 +00:00
|
|
|
got = self.post('/user.com/inbox', json=undo_follow)
|
2023-01-10 06:58:30 +00:00
|
|
|
self.assertEqual(200, got.status_code)
|
|
|
|
|
2018-10-23 14:11:44 +00:00
|
|
|
def test_inbox_unsupported_type(self, *_):
|
2023-03-19 22:43:55 +00:00
|
|
|
got = self.post('/user.com/inbox', json={
|
2017-10-17 05:21:13 +00:00
|
|
|
'@context': ['https://www.w3.org/ns/activitystreams'],
|
|
|
|
'id': 'https://xoxo.zone/users/aaronpk#follows/40',
|
2018-10-21 22:28:42 +00:00
|
|
|
'type': 'Block',
|
2017-10-17 05:21:13 +00:00
|
|
|
'actor': 'https://xoxo.zone/users/aaronpk',
|
|
|
|
'object': 'http://snarfed.org/',
|
2021-07-10 15:07:40 +00:00
|
|
|
})
|
|
|
|
self.assertEqual(501, got.status_code)
|
2020-03-01 04:28:53 +00:00
|
|
|
|
2022-12-10 17:01:04 +00:00
|
|
|
def test_inbox_bad_object_url(self, mock_head, mock_get, mock_post):
|
|
|
|
# https://console.cloud.google.com/errors/detail/CMKn7tqbq-GIRA;time=P30D?project=bridgy-federated
|
2023-01-07 17:18:11 +00:00
|
|
|
mock_get.return_value = self.as2_resp(ACTOR) # source actor
|
2023-02-12 06:53:50 +00:00
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
id = 'https://mas.to/users/tmichellemoore#likes/56486252'
|
2023-02-12 06:53:50 +00:00
|
|
|
bad_url = 'http://localhost/r/Testing \u2013 Brid.gy \u2013 Post to Mastodon 3'
|
2023-03-19 22:43:55 +00:00
|
|
|
got = self.post('/user.com/inbox', json={
|
2022-12-10 17:01:04 +00:00
|
|
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
2023-02-12 06:53:50 +00:00
|
|
|
'id': id,
|
2022-12-10 17:01:04 +00:00
|
|
|
'type': 'Like',
|
|
|
|
'actor': ACTOR['id'],
|
2023-02-12 06:53:50 +00:00
|
|
|
'object': bad_url,
|
2022-12-10 17:01:04 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
# bad object, should ignore activity
|
|
|
|
self.assertEqual(200, got.status_code)
|
|
|
|
mock_post.assert_not_called()
|
2023-02-12 06:53:50 +00:00
|
|
|
|
|
|
|
obj = Object.get_by_id(id)
|
2023-02-19 01:53:27 +00:00
|
|
|
self.assertEqual(['activity'], obj.labels)
|
2023-02-12 06:53:50 +00:00
|
|
|
self.assertEqual([], obj.domains)
|
|
|
|
|
|
|
|
self.assertIsNone(Object.get_by_id(bad_url))
|
2022-12-10 17:01:04 +00:00
|
|
|
|
2023-02-16 04:10:17 +00:00
|
|
|
@patch('activitypub.logger.info', side_effect=logging.info)
|
2023-03-08 21:10:41 +00:00
|
|
|
@patch('common.logger.info', side_effect=logging.info)
|
|
|
|
def test_inbox_verify_http_signature(self, mock_common_log, mock_activitypub_log,
|
|
|
|
_, mock_get, ___):
|
2023-02-15 18:57:11 +00:00
|
|
|
# actor with a public key
|
2023-03-29 19:48:50 +00:00
|
|
|
self.key_id_obj.key.delete()
|
2023-04-03 03:36:23 +00:00
|
|
|
protocol.objects_cache.clear()
|
2023-02-15 18:57:11 +00:00
|
|
|
mock_get.return_value = self.as2_resp({
|
|
|
|
**ACTOR,
|
|
|
|
'publicKey': {
|
2023-02-16 04:10:17 +00:00
|
|
|
'id': 'http://my/key/id#unused',
|
|
|
|
'owner': 'http://own/er',
|
2023-02-15 18:57:11 +00:00
|
|
|
'publicKeyPem': self.user.public_pem().decode(),
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
# valid signature
|
2023-02-26 13:34:15 +00:00
|
|
|
body = json_dumps(NOTE)
|
|
|
|
headers = self.sign('/inbox', json_dumps(NOTE))
|
2023-02-15 22:04:17 +00:00
|
|
|
resp = self.client.post('/inbox', data=body, headers=headers)
|
2023-02-15 20:41:29 +00:00
|
|
|
self.assertEqual(200, resp.status_code, resp.get_data(as_text=True))
|
2023-02-16 04:10:17 +00:00
|
|
|
mock_get.assert_has_calls((
|
|
|
|
self.as2_req('http://my/key/id'),
|
|
|
|
))
|
2023-03-08 21:10:41 +00:00
|
|
|
mock_activitypub_log.assert_any_call('HTTP Signature verified!')
|
2023-02-15 18:57:11 +00:00
|
|
|
|
2023-02-16 04:10:17 +00:00
|
|
|
# invalid signature, missing keyId
|
2023-03-08 21:10:41 +00:00
|
|
|
protocol.seen_ids.clear()
|
2023-02-16 04:10:17 +00:00
|
|
|
obj_key = ndb.Key(Object, NOTE['id'])
|
|
|
|
obj_key.delete()
|
|
|
|
|
|
|
|
resp = self.client.post('/inbox', data=body, headers={
|
|
|
|
**headers,
|
|
|
|
'signature': headers['signature'].replace(
|
|
|
|
'keyId="http://my/key/id#unused",', ''),
|
|
|
|
})
|
2023-02-26 13:34:15 +00:00
|
|
|
self.assertEqual(401, resp.status_code)
|
|
|
|
self.assertEqual({'error': 'HTTP Signature missing keyId'}, resp.json)
|
2023-04-04 14:14:31 +00:00
|
|
|
mock_common_log.assert_any_call('Returning 401: HTTP Signature missing keyId', exc_info=None)
|
2023-02-16 04:10:17 +00:00
|
|
|
|
2023-02-15 18:57:11 +00:00
|
|
|
# invalid signature, content changed
|
2023-03-08 21:10:41 +00:00
|
|
|
protocol.seen_ids.clear()
|
2023-02-15 18:57:11 +00:00
|
|
|
obj_key = ndb.Key(Object, NOTE['id'])
|
|
|
|
obj_key.delete()
|
2023-02-15 20:41:29 +00:00
|
|
|
|
2023-02-15 22:04:17 +00:00
|
|
|
resp = self.client.post('/inbox', json={**NOTE, 'content': 'z'}, headers=headers)
|
2023-02-26 13:34:15 +00:00
|
|
|
self.assertEqual(401, resp.status_code)
|
|
|
|
self.assertEqual({'error': 'Invalid Digest header, required for HTTP Signature'},
|
|
|
|
resp.json)
|
2023-04-04 14:14:31 +00:00
|
|
|
mock_common_log.assert_any_call('Returning 401: Invalid Digest header, required for HTTP Signature', exc_info=None)
|
2023-02-15 18:57:11 +00:00
|
|
|
|
|
|
|
# invalid signature, header changed
|
2023-03-08 21:10:41 +00:00
|
|
|
protocol.seen_ids.clear()
|
2023-02-15 18:57:11 +00:00
|
|
|
obj_key.delete()
|
|
|
|
orig_date = headers['Date']
|
2023-02-15 20:41:29 +00:00
|
|
|
|
2023-02-15 22:04:17 +00:00
|
|
|
resp = self.client.post('/inbox', data=body, headers={**headers, 'Date': 'X'})
|
2023-02-26 13:34:15 +00:00
|
|
|
self.assertEqual(401, resp.status_code)
|
|
|
|
self.assertEqual({'error': 'HTTP Signature verification failed'}, resp.json)
|
2023-04-04 14:14:31 +00:00
|
|
|
mock_common_log.assert_any_call('Returning 401: HTTP Signature verification failed', exc_info=None)
|
2023-02-15 20:41:29 +00:00
|
|
|
|
|
|
|
# no signature
|
2023-03-08 21:10:41 +00:00
|
|
|
protocol.seen_ids.clear()
|
2023-02-15 20:41:29 +00:00
|
|
|
obj_key.delete()
|
2023-02-15 22:04:17 +00:00
|
|
|
resp = self.client.post('/inbox', json=NOTE)
|
2023-02-26 13:34:15 +00:00
|
|
|
self.assertEqual(401, resp.status_code, resp.get_data(as_text=True))
|
|
|
|
self.assertEqual({'error': 'No HTTP Signature'}, resp.json)
|
2023-04-04 14:14:31 +00:00
|
|
|
mock_common_log.assert_any_call('Returning 401: No HTTP Signature', exc_info=None)
|
2023-02-15 18:57:11 +00:00
|
|
|
|
2023-03-10 03:56:04 +00:00
|
|
|
def test_delete_actor(self, *mocks):
|
2023-03-19 22:43:55 +00:00
|
|
|
follower = Follower.get_or_create('user.com', DELETE['actor'])
|
2022-11-21 20:14:37 +00:00
|
|
|
followee = Follower.get_or_create(DELETE['actor'], 'snarfed.org')
|
|
|
|
# other unrelated follower
|
2023-03-19 22:43:55 +00:00
|
|
|
other = Follower.get_or_create('user.com', 'https://mas.to/users/other')
|
2022-11-21 20:14:37 +00:00
|
|
|
self.assertEqual(3, Follower.query().count())
|
|
|
|
|
2023-02-26 13:34:15 +00:00
|
|
|
got = self.post('/inbox', json=DELETE)
|
2022-11-21 20:14:37 +00:00
|
|
|
self.assertEqual(200, got.status_code)
|
|
|
|
self.assertEqual('inactive', follower.key.get().status)
|
|
|
|
self.assertEqual('inactive', followee.key.get().status)
|
|
|
|
self.assertEqual('active', other.key.get().status)
|
2020-03-01 04:28:53 +00:00
|
|
|
|
2023-03-27 21:18:24 +00:00
|
|
|
def test_delete_actor_not_fetchable(self, _, mock_get, ___):
|
2023-03-29 19:48:50 +00:00
|
|
|
self.key_id_obj.key.delete()
|
2023-04-03 03:36:23 +00:00
|
|
|
protocol.objects_cache.clear()
|
2023-03-10 03:56:04 +00:00
|
|
|
|
|
|
|
mock_get.return_value = requests_response(status=410)
|
|
|
|
got = self.post('/inbox', json={**DELETE, 'object': 'http://my/key/id'})
|
|
|
|
self.assertEqual(202, got.status_code)
|
|
|
|
|
2023-03-29 19:48:50 +00:00
|
|
|
def test_delete_actor_empty_deleted_object(self, _, mock_get, ___):
|
|
|
|
self.key_id_obj.as2 = None
|
|
|
|
self.key_id_obj.deleted = True
|
|
|
|
self.key_id_obj.put()
|
2023-04-03 03:36:23 +00:00
|
|
|
protocol.objects_cache.clear()
|
2023-03-29 19:48:50 +00:00
|
|
|
|
|
|
|
got = self.post('/inbox', json={**DELETE, 'object': 'http://my/key/id'})
|
|
|
|
self.assertEqual(202, got.status_code)
|
|
|
|
mock_get.assert_not_called()
|
|
|
|
|
2023-02-15 18:57:11 +00:00
|
|
|
def test_delete_note(self, _, mock_get, ___):
|
2023-02-24 13:25:29 +00:00
|
|
|
obj = Object(id='http://an/obj', as2={})
|
2023-02-14 22:30:00 +00:00
|
|
|
obj.put()
|
2023-02-12 05:46:47 +00:00
|
|
|
|
2023-02-15 18:57:11 +00:00
|
|
|
mock_get.side_effect = [
|
|
|
|
self.as2_resp(ACTOR),
|
|
|
|
]
|
|
|
|
|
2023-02-12 06:53:50 +00:00
|
|
|
delete = {
|
2023-02-12 05:46:47 +00:00
|
|
|
**DELETE,
|
|
|
|
'object': 'http://an/obj',
|
2023-02-12 06:53:50 +00:00
|
|
|
}
|
2023-02-26 13:34:15 +00:00
|
|
|
resp = self.post('/inbox', json=delete)
|
2023-02-12 05:46:47 +00:00
|
|
|
self.assertEqual(200, resp.status_code)
|
2023-02-14 22:30:00 +00:00
|
|
|
self.assertTrue(obj.key.get().deleted)
|
2023-03-28 04:51:18 +00:00
|
|
|
self.assert_object(delete['id'], as2=delete, type='delete',
|
|
|
|
source_protocol='activitypub', status='complete',
|
|
|
|
labels=['activity'])
|
2023-02-12 05:46:47 +00:00
|
|
|
|
2023-02-14 22:30:00 +00:00
|
|
|
obj.deleted = True
|
2023-04-03 03:36:23 +00:00
|
|
|
self.assert_entities_equal(obj, protocol.objects_cache['http://an/obj'])
|
2023-02-14 22:30:00 +00:00
|
|
|
|
2023-02-15 18:57:11 +00:00
|
|
|
def test_update_note(self, *mocks):
|
2023-02-24 13:25:29 +00:00
|
|
|
Object(id='https://a/note', as2={}).put()
|
2023-02-15 18:57:11 +00:00
|
|
|
self._test_update(*mocks)
|
2023-02-12 06:23:01 +00:00
|
|
|
|
2023-02-15 18:57:11 +00:00
|
|
|
def test_update_unknown(self, *mocks):
|
|
|
|
self._test_update(*mocks)
|
|
|
|
|
|
|
|
def _test_update(self, _, mock_get, ___):
|
|
|
|
mock_get.side_effect = [
|
|
|
|
self.as2_resp(ACTOR),
|
|
|
|
]
|
2023-02-12 06:23:01 +00:00
|
|
|
|
2023-02-26 13:34:15 +00:00
|
|
|
resp = self.post('/inbox', json=UPDATE_NOTE)
|
2023-02-12 06:23:01 +00:00
|
|
|
self.assertEqual(200, resp.status_code)
|
|
|
|
|
|
|
|
obj = UPDATE_NOTE['object']
|
|
|
|
self.assert_object('https://a/note', type='note', as2=obj,
|
2023-02-24 03:17:26 +00:00
|
|
|
source_protocol='activitypub')
|
2023-02-12 06:53:50 +00:00
|
|
|
self.assert_object(UPDATE_NOTE['id'], source_protocol='activitypub',
|
2023-03-28 04:51:18 +00:00
|
|
|
type='update', status='complete', as2=UPDATE_NOTE,
|
|
|
|
labels=['activity'])
|
2023-01-26 03:44:48 +00:00
|
|
|
|
2023-02-14 22:30:00 +00:00
|
|
|
self.assert_entities_equal(Object.get_by_id('https://a/note'),
|
2023-04-03 03:36:23 +00:00
|
|
|
protocol.objects_cache['https://a/note'])
|
2023-02-14 22:30:00 +00:00
|
|
|
|
2021-09-01 15:19:38 +00:00
|
|
|
def test_inbox_webmention_discovery_connection_fails(self, mock_head,
|
|
|
|
mock_get, mock_post):
|
|
|
|
mock_get.side_effect = [
|
|
|
|
# source actor
|
2023-01-07 17:18:11 +00:00
|
|
|
self.as2_resp(LIKE_WITH_ACTOR['actor']),
|
2021-09-01 15:19:38 +00:00
|
|
|
# target post webmention discovery
|
|
|
|
ReadTimeoutError(None, None, None),
|
|
|
|
]
|
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
got = self.post('/user.com/inbox', json=LIKE)
|
2021-09-01 15:19:38 +00:00
|
|
|
self.assertEqual(504, got.status_code)
|
|
|
|
|
2022-01-12 06:40:24 +00:00
|
|
|
def test_inbox_no_webmention_endpoint(self, mock_head, mock_get, mock_post):
|
|
|
|
mock_get.side_effect = [
|
|
|
|
# source actor
|
2023-01-07 17:18:11 +00:00
|
|
|
self.as2_resp(LIKE_WITH_ACTOR['actor']),
|
2022-01-12 06:40:24 +00:00
|
|
|
# target post webmention discovery
|
2023-03-19 22:43:55 +00:00
|
|
|
HTML,
|
2022-01-12 06:40:24 +00:00
|
|
|
]
|
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
got = self.post('/user.com/inbox', json=LIKE)
|
2022-01-12 06:40:24 +00:00
|
|
|
self.assertEqual(200, got.status_code)
|
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
self.assert_object('http://mas.to/like#ok',
|
|
|
|
domains=['user.com'],
|
2023-01-29 22:13:58 +00:00
|
|
|
source_protocol='activitypub',
|
2023-02-19 01:53:27 +00:00
|
|
|
status='complete',
|
2023-01-29 22:13:58 +00:00
|
|
|
as2=LIKE_WITH_ACTOR,
|
2023-01-30 01:30:11 +00:00
|
|
|
type='like',
|
2023-03-19 22:43:55 +00:00
|
|
|
labels=['activity', 'notification'],
|
2023-01-30 01:30:11 +00:00
|
|
|
object_ids=[LIKE['object']])
|
2022-11-22 02:46:10 +00:00
|
|
|
|
2023-02-13 05:58:59 +00:00
|
|
|
def test_inbox_id_already_seen(self, *mocks):
|
2023-02-24 13:25:29 +00:00
|
|
|
obj_key = Object(id=FOLLOW_WRAPPED['id'], as2={}).put()
|
2023-02-13 05:58:59 +00:00
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
got = self.post('/user.com/inbox', json=FOLLOW_WRAPPED)
|
2023-02-14 03:10:01 +00:00
|
|
|
self.assertEqual(200, got.status_code)
|
2023-02-13 05:58:59 +00:00
|
|
|
self.assertEqual(0, Follower.query().count())
|
|
|
|
|
2023-02-13 06:17:04 +00:00
|
|
|
# second time should use in memory cache
|
|
|
|
obj_key.delete()
|
2023-03-19 22:43:55 +00:00
|
|
|
got = self.post('/user.com/inbox', json=FOLLOW_WRAPPED)
|
2023-02-14 03:10:01 +00:00
|
|
|
self.assertEqual(200, got.status_code)
|
2023-02-13 06:17:04 +00:00
|
|
|
self.assertEqual(0, Follower.query().count())
|
|
|
|
|
2022-11-22 02:46:10 +00:00
|
|
|
def test_followers_collection_unknown_user(self, *args):
|
2023-02-08 02:25:24 +00:00
|
|
|
resp = self.client.get('/nope.com/followers')
|
2022-11-22 02:46:10 +00:00
|
|
|
self.assertEqual(404, resp.status_code)
|
|
|
|
|
2023-01-19 15:00:13 +00:00
|
|
|
def test_followers_collection_empty(self, *args):
|
2023-03-19 22:43:55 +00:00
|
|
|
resp = self.client.get('/user.com/followers')
|
2022-11-22 02:46:10 +00:00
|
|
|
self.assertEqual(200, resp.status_code)
|
|
|
|
self.assertEqual({
|
|
|
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
2023-03-19 22:43:55 +00:00
|
|
|
'id': 'http://localhost/user.com/followers',
|
2022-11-22 02:46:10 +00:00
|
|
|
'type': 'Collection',
|
2023-03-19 22:43:55 +00:00
|
|
|
'summary': "user.com's followers",
|
2022-11-22 02:46:10 +00:00
|
|
|
'totalItems': 0,
|
2023-01-20 15:02:55 +00:00
|
|
|
'first': {
|
|
|
|
'type': 'CollectionPage',
|
2023-03-19 22:43:55 +00:00
|
|
|
'partOf': 'http://localhost/user.com/followers',
|
2023-01-20 15:02:55 +00:00
|
|
|
'items': [],
|
|
|
|
},
|
2022-11-22 02:46:10 +00:00
|
|
|
}, resp.json)
|
|
|
|
|
2023-01-20 15:02:55 +00:00
|
|
|
def store_followers(self):
|
2023-03-19 22:43:55 +00:00
|
|
|
Follower.get_or_create('user.com', 'https://bar.com',
|
2023-02-24 13:25:29 +00:00
|
|
|
last_follow=FOLLOW_WITH_ACTOR)
|
2023-03-19 22:43:55 +00:00
|
|
|
Follower.get_or_create('http://other/actor', 'user.com')
|
|
|
|
Follower.get_or_create('user.com', 'https://baz.com',
|
2023-02-24 13:25:29 +00:00
|
|
|
last_follow=FOLLOW_WITH_ACTOR)
|
2023-03-19 22:43:55 +00:00
|
|
|
Follower.get_or_create('user.com', 'baj.com', status='inactive')
|
2022-11-22 02:46:10 +00:00
|
|
|
|
2023-01-20 15:02:55 +00:00
|
|
|
def test_followers_collection(self, *args):
|
|
|
|
self.store_followers()
|
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
resp = self.client.get('/user.com/followers')
|
2022-11-22 02:46:10 +00:00
|
|
|
self.assertEqual(200, resp.status_code)
|
|
|
|
self.assertEqual({
|
|
|
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
2023-03-19 22:43:55 +00:00
|
|
|
'id': 'http://localhost/user.com/followers',
|
2022-11-22 02:46:10 +00:00
|
|
|
'type': 'Collection',
|
2023-03-19 22:43:55 +00:00
|
|
|
'summary': "user.com's followers",
|
2022-11-22 02:46:10 +00:00
|
|
|
'totalItems': 2,
|
2023-01-20 15:02:55 +00:00
|
|
|
'first': {
|
|
|
|
'type': 'CollectionPage',
|
2023-03-19 22:43:55 +00:00
|
|
|
'partOf': 'http://localhost/user.com/followers',
|
2023-01-20 15:02:55 +00:00
|
|
|
'items': [ACTOR, ACTOR],
|
|
|
|
},
|
|
|
|
}, resp.json)
|
|
|
|
|
2023-03-08 21:10:41 +00:00
|
|
|
@patch('models.PAGE_SIZE', 1)
|
2023-01-20 15:02:55 +00:00
|
|
|
def test_followers_collection_page(self, *args):
|
|
|
|
self.store_followers()
|
|
|
|
before = (datetime.utcnow() + timedelta(seconds=1)).isoformat()
|
2023-03-19 22:43:55 +00:00
|
|
|
next = Follower.get_by_id('user.com https://baz.com').updated.isoformat()
|
2023-01-20 15:02:55 +00:00
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
resp = self.client.get(f'/user.com/followers?before={before}')
|
2023-01-20 15:02:55 +00:00
|
|
|
self.assertEqual(200, resp.status_code)
|
|
|
|
self.assertEqual({
|
|
|
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
2023-03-19 22:43:55 +00:00
|
|
|
'id': f'http://localhost/user.com/followers?before={before}',
|
2023-01-20 15:02:55 +00:00
|
|
|
'type': 'CollectionPage',
|
2023-03-19 22:43:55 +00:00
|
|
|
'partOf': 'http://localhost/user.com/followers',
|
|
|
|
'next': f'http://localhost/user.com/followers?before={next}',
|
|
|
|
'prev': f'http://localhost/user.com/followers?after={before}',
|
2023-01-20 15:02:55 +00:00
|
|
|
'items': [ACTOR],
|
2022-11-22 02:46:10 +00:00
|
|
|
}, resp.json)
|
|
|
|
|
|
|
|
def test_following_collection_unknown_user(self, *args):
|
2023-02-08 02:25:24 +00:00
|
|
|
resp = self.client.get('/nope.com/following')
|
2022-11-22 02:46:10 +00:00
|
|
|
self.assertEqual(404, resp.status_code)
|
|
|
|
|
2023-01-19 15:00:13 +00:00
|
|
|
def test_following_collection_empty(self, *args):
|
2023-03-19 22:43:55 +00:00
|
|
|
resp = self.client.get('/user.com/following')
|
2022-11-22 02:46:10 +00:00
|
|
|
self.assertEqual(200, resp.status_code)
|
|
|
|
self.assertEqual({
|
|
|
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
2023-03-19 22:43:55 +00:00
|
|
|
'id': 'http://localhost/user.com/following',
|
|
|
|
'summary': "user.com's following",
|
2022-11-22 02:46:10 +00:00
|
|
|
'type': 'Collection',
|
|
|
|
'totalItems': 0,
|
2023-01-20 15:02:55 +00:00
|
|
|
'first': {
|
|
|
|
'type': 'CollectionPage',
|
2023-03-19 22:43:55 +00:00
|
|
|
'partOf': 'http://localhost/user.com/following',
|
2023-01-20 15:02:55 +00:00
|
|
|
'items': [],
|
|
|
|
},
|
2022-11-22 02:46:10 +00:00
|
|
|
}, resp.json)
|
|
|
|
|
2023-01-20 15:02:55 +00:00
|
|
|
def store_following(self):
|
2023-03-19 22:43:55 +00:00
|
|
|
Follower.get_or_create('https://bar.com', 'user.com',
|
2023-02-24 13:25:29 +00:00
|
|
|
last_follow=FOLLOW_WITH_OBJECT)
|
2023-03-19 22:43:55 +00:00
|
|
|
Follower.get_or_create('user.com', 'http://other/actor')
|
|
|
|
Follower.get_or_create('https://baz.com', 'user.com',
|
2023-02-24 13:25:29 +00:00
|
|
|
last_follow=FOLLOW_WITH_OBJECT)
|
2023-03-19 22:43:55 +00:00
|
|
|
Follower.get_or_create('baj.com', 'user.com', status='inactive')
|
2022-11-22 02:46:10 +00:00
|
|
|
|
2023-01-20 15:02:55 +00:00
|
|
|
def test_following_collection(self, *args):
|
|
|
|
self.store_following()
|
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
resp = self.client.get('/user.com/following')
|
2022-11-22 02:46:10 +00:00
|
|
|
self.assertEqual(200, resp.status_code)
|
|
|
|
self.assertEqual({
|
|
|
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
2023-03-19 22:43:55 +00:00
|
|
|
'id': 'http://localhost/user.com/following',
|
|
|
|
'summary': "user.com's following",
|
2022-11-22 02:46:10 +00:00
|
|
|
'type': 'Collection',
|
|
|
|
'totalItems': 2,
|
2023-01-20 15:02:55 +00:00
|
|
|
'first': {
|
|
|
|
'type': 'CollectionPage',
|
2023-03-19 22:43:55 +00:00
|
|
|
'partOf': 'http://localhost/user.com/following',
|
2023-01-20 15:02:55 +00:00
|
|
|
'items': [ACTOR, ACTOR],
|
|
|
|
},
|
|
|
|
}, resp.json)
|
|
|
|
|
2023-03-08 21:10:41 +00:00
|
|
|
@patch('models.PAGE_SIZE', 1)
|
2023-01-20 15:02:55 +00:00
|
|
|
def test_following_collection_page(self, *args):
|
|
|
|
self.store_following()
|
|
|
|
after = datetime(1900, 1, 1).isoformat()
|
2023-03-19 22:43:55 +00:00
|
|
|
prev = Follower.get_by_id('https://baz.com user.com').updated.isoformat()
|
2023-01-20 15:02:55 +00:00
|
|
|
|
2023-03-19 22:43:55 +00:00
|
|
|
resp = self.client.get(f'/user.com/following?after={after}')
|
2023-01-20 15:02:55 +00:00
|
|
|
self.assertEqual(200, resp.status_code)
|
|
|
|
self.assertEqual({
|
|
|
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
2023-03-19 22:43:55 +00:00
|
|
|
'id': f'http://localhost/user.com/following?after={after}',
|
2023-01-20 15:02:55 +00:00
|
|
|
'type': 'CollectionPage',
|
2023-03-19 22:43:55 +00:00
|
|
|
'partOf': 'http://localhost/user.com/following',
|
|
|
|
'prev': f'http://localhost/user.com/following?after={prev}',
|
|
|
|
'next': f'http://localhost/user.com/following?before={after}',
|
2023-01-20 15:02:55 +00:00
|
|
|
'items': [ACTOR],
|
2022-11-22 02:46:10 +00:00
|
|
|
}, resp.json)
|
2023-01-25 21:12:24 +00:00
|
|
|
|
|
|
|
def test_outbox_empty(self, _, mock_get, __):
|
2023-03-19 22:43:55 +00:00
|
|
|
resp = self.client.get(f'/user.com/outbox')
|
2023-01-25 21:12:24 +00:00
|
|
|
self.assertEqual(200, resp.status_code)
|
|
|
|
self.assertEqual({
|
|
|
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
2023-03-19 22:43:55 +00:00
|
|
|
'id': 'http://localhost/user.com/outbox',
|
|
|
|
'summary': "user.com's outbox",
|
2023-01-25 21:12:24 +00:00
|
|
|
'type': 'OrderedCollection',
|
|
|
|
'totalItems': 0,
|
|
|
|
'first': {
|
|
|
|
'type': 'CollectionPage',
|
2023-03-19 22:43:55 +00:00
|
|
|
'partOf': 'http://localhost/user.com/outbox',
|
2023-01-25 21:12:24 +00:00
|
|
|
'items': [],
|
|
|
|
},
|
|
|
|
}, resp.json)
|
2023-03-08 21:10:41 +00:00
|
|
|
|
|
|
|
|
2023-05-26 23:07:36 +00:00
|
|
|
class ActivityPubUtilsTest(TestCase):
|
2023-03-08 21:10:41 +00:00
|
|
|
def setUp(self):
|
|
|
|
super().setUp()
|
2023-05-23 06:09:36 +00:00
|
|
|
self.request_context.push()
|
2023-05-26 23:07:36 +00:00
|
|
|
g.user = self.make_user('user.com', has_hcard=True,
|
|
|
|
actor_as2=ACTOR)
|
2023-03-08 21:10:41 +00:00
|
|
|
def tearDown(self):
|
2023-05-23 06:09:36 +00:00
|
|
|
self.request_context.pop()
|
2023-03-08 21:10:41 +00:00
|
|
|
super().tearDown()
|
|
|
|
|
|
|
|
def test_postprocess_as2_multiple_in_reply_tos(self):
|
|
|
|
self.assert_equals({
|
|
|
|
'id': 'http://localhost/r/xyz',
|
|
|
|
'inReplyTo': 'foo',
|
|
|
|
'to': [as2.PUBLIC_AUDIENCE],
|
|
|
|
}, activitypub.postprocess_as2({
|
|
|
|
'id': 'xyz',
|
|
|
|
'inReplyTo': ['foo', 'bar'],
|
2023-03-20 21:28:14 +00:00
|
|
|
}))
|
2023-03-08 21:10:41 +00:00
|
|
|
|
|
|
|
def test_postprocess_as2_multiple_url(self):
|
|
|
|
self.assert_equals({
|
|
|
|
'id': 'http://localhost/r/xyz',
|
|
|
|
'url': ['http://localhost/r/foo', 'http://localhost/r/bar'],
|
|
|
|
'to': [as2.PUBLIC_AUDIENCE],
|
|
|
|
}, activitypub.postprocess_as2({
|
|
|
|
'id': 'xyz',
|
|
|
|
'url': ['foo', 'bar'],
|
2023-03-20 21:28:14 +00:00
|
|
|
}))
|
2023-03-08 21:10:41 +00:00
|
|
|
|
|
|
|
def test_postprocess_as2_multiple_image(self):
|
|
|
|
self.assert_equals({
|
|
|
|
'id': 'http://localhost/r/xyz',
|
|
|
|
'attachment': [{'url': 'http://r/foo'}, {'url': 'http://r/bar'}],
|
|
|
|
'image': [{'url': 'http://r/foo'}, {'url': 'http://r/bar'}],
|
|
|
|
'to': [as2.PUBLIC_AUDIENCE],
|
|
|
|
}, activitypub.postprocess_as2({
|
|
|
|
'id': 'xyz',
|
|
|
|
'image': [{'url': 'http://r/foo'}, {'url': 'http://r/bar'}],
|
2023-03-20 21:28:14 +00:00
|
|
|
}))
|
2023-03-08 21:10:41 +00:00
|
|
|
|
|
|
|
def test_postprocess_as2_actor_attributedTo(self):
|
2023-05-26 23:07:36 +00:00
|
|
|
g.user = Fake(id='site')
|
2023-03-08 21:10:41 +00:00
|
|
|
self.assert_equals({
|
|
|
|
'actor': {
|
|
|
|
'id': 'baj',
|
|
|
|
'preferredUsername': 'site',
|
|
|
|
'url': 'http://localhost/r/https://site/',
|
|
|
|
},
|
|
|
|
'attributedTo': [{
|
|
|
|
'id': 'bar',
|
|
|
|
'preferredUsername': 'site',
|
|
|
|
'url': 'http://localhost/r/https://site/',
|
|
|
|
}, {
|
|
|
|
'id': 'baz',
|
|
|
|
'preferredUsername': 'site',
|
|
|
|
'url': 'http://localhost/r/https://site/',
|
|
|
|
}],
|
|
|
|
'to': [as2.PUBLIC_AUDIENCE],
|
|
|
|
}, activitypub.postprocess_as2({
|
|
|
|
'attributedTo': [{'id': 'bar'}, {'id': 'baz'}],
|
|
|
|
'actor': {'id': 'baj'},
|
2023-03-20 21:28:14 +00:00
|
|
|
}))
|
2023-03-08 21:10:41 +00:00
|
|
|
|
|
|
|
def test_postprocess_as2_note(self):
|
|
|
|
self.assert_equals({
|
2023-04-02 02:13:51 +00:00
|
|
|
'id': 'http://localhost/r/xyz',
|
|
|
|
'type': 'Note',
|
|
|
|
'to': [as2.PUBLIC_AUDIENCE],
|
2023-03-08 21:10:41 +00:00
|
|
|
}, activitypub.postprocess_as2({
|
|
|
|
'id': 'xyz',
|
|
|
|
'type': 'Note',
|
2023-03-20 21:28:14 +00:00
|
|
|
}))
|
2023-03-08 21:10:41 +00:00
|
|
|
|
2023-03-14 21:59:28 +00:00
|
|
|
def test_postprocess_as2_hashtag(self):
|
|
|
|
"""https://github.com/snarfed/bridgy-fed/issues/45"""
|
|
|
|
self.assert_equals({
|
|
|
|
'tag': [
|
|
|
|
{'type': 'Hashtag', 'name': '#bar', 'href': 'bar'},
|
|
|
|
{'type': 'Hashtag', 'name': '#baz', 'href': 'http://localhost/hashtag/baz'},
|
|
|
|
{'type': 'Mention', 'href': 'foo'},
|
|
|
|
],
|
|
|
|
'to': ['https://www.w3.org/ns/activitystreams#Public'],
|
|
|
|
}, activitypub.postprocess_as2({
|
|
|
|
'tag': [
|
|
|
|
{'name': 'bar', 'href': 'bar'},
|
|
|
|
{'type': 'Tag','name': '#baz'},
|
|
|
|
# should leave alone
|
|
|
|
{'type': 'Mention', 'href': 'foo'},
|
|
|
|
],
|
2023-03-20 21:28:14 +00:00
|
|
|
}))
|
2023-03-14 21:59:28 +00:00
|
|
|
|
2023-05-26 23:07:36 +00:00
|
|
|
# TODO: make these generic and use Fake
|
2023-03-08 21:10:41 +00:00
|
|
|
@patch('requests.get')
|
2023-03-29 20:13:32 +00:00
|
|
|
def test_load_http(self, mock_get):
|
2023-03-08 21:10:41 +00:00
|
|
|
mock_get.return_value = AS2
|
|
|
|
|
|
|
|
id = 'http://the/id'
|
|
|
|
self.assertIsNone(Object.get_by_id(id))
|
|
|
|
|
|
|
|
# first time fetches over HTTP
|
2023-03-29 20:13:32 +00:00
|
|
|
got = ActivityPub.load(id)
|
2023-03-08 21:10:41 +00:00
|
|
|
self.assert_equals(id, got.key.id())
|
|
|
|
self.assert_equals(AS2_OBJ, got.as2)
|
|
|
|
mock_get.assert_has_calls([self.as2_req(id)])
|
|
|
|
|
|
|
|
# second time is in cache
|
|
|
|
got.key.delete()
|
|
|
|
mock_get.reset_mock()
|
|
|
|
|
2023-03-29 20:13:32 +00:00
|
|
|
got = ActivityPub.load(id)
|
2023-03-08 21:10:41 +00:00
|
|
|
self.assert_equals(id, got.key.id())
|
|
|
|
self.assert_equals(AS2_OBJ, got.as2)
|
|
|
|
mock_get.assert_not_called()
|
|
|
|
|
|
|
|
@patch('requests.get')
|
2023-03-29 20:13:32 +00:00
|
|
|
def test_load_datastore(self, mock_get):
|
2023-03-08 21:10:41 +00:00
|
|
|
id = 'http://the/id'
|
|
|
|
stored = Object(id=id, as2=AS2_OBJ)
|
|
|
|
stored.put()
|
2023-04-03 03:36:23 +00:00
|
|
|
protocol.objects_cache.clear()
|
2023-03-08 21:10:41 +00:00
|
|
|
|
|
|
|
# first time loads from datastore
|
2023-03-29 20:13:32 +00:00
|
|
|
got = ActivityPub.load(id)
|
2023-03-08 21:10:41 +00:00
|
|
|
self.assert_entities_equal(stored, got)
|
|
|
|
mock_get.assert_not_called()
|
|
|
|
|
|
|
|
# second time is in cache
|
|
|
|
stored.key.delete()
|
2023-03-29 20:13:32 +00:00
|
|
|
got = ActivityPub.load(id)
|
2023-03-08 21:10:41 +00:00
|
|
|
self.assert_entities_equal(stored, got)
|
|
|
|
mock_get.assert_not_called()
|
|
|
|
|
|
|
|
@patch('requests.get')
|
2023-04-03 14:53:15 +00:00
|
|
|
def test_load_preserves_fragment(self, mock_get):
|
|
|
|
stored = Object(id='http://the/id#frag', as2=AS2_OBJ)
|
2023-03-08 21:10:41 +00:00
|
|
|
stored.put()
|
2023-04-03 03:36:23 +00:00
|
|
|
protocol.objects_cache.clear()
|
2023-03-08 21:10:41 +00:00
|
|
|
|
2023-04-03 14:53:15 +00:00
|
|
|
got = ActivityPub.load('http://the/id#frag')
|
2023-03-08 21:10:41 +00:00
|
|
|
self.assert_entities_equal(stored, got)
|
|
|
|
mock_get.assert_not_called()
|
|
|
|
|
|
|
|
@patch('requests.get')
|
2023-03-29 20:13:32 +00:00
|
|
|
def test_load_datastore_no_as2(self, mock_get):
|
2023-03-08 21:10:41 +00:00
|
|
|
"""If the stored Object has no as2, we should fall back to HTTP."""
|
|
|
|
id = 'http://the/id'
|
|
|
|
stored = Object(id=id, as2={}, status='in progress')
|
|
|
|
stored.put()
|
2023-04-03 03:36:23 +00:00
|
|
|
protocol.objects_cache.clear()
|
2023-03-08 21:10:41 +00:00
|
|
|
|
|
|
|
mock_get.return_value = AS2
|
2023-03-29 20:13:32 +00:00
|
|
|
got = ActivityPub.load(id)
|
2023-03-08 21:10:41 +00:00
|
|
|
mock_get.assert_has_calls([self.as2_req(id)])
|
|
|
|
|
|
|
|
self.assert_equals(id, got.key.id())
|
|
|
|
self.assert_equals(AS2_OBJ, got.as2)
|
|
|
|
mock_get.assert_has_calls([self.as2_req(id)])
|
|
|
|
|
2023-03-21 02:17:55 +00:00
|
|
|
self.assert_object(id, delivered_protocol='webmention',
|
|
|
|
as2=AS2_OBJ, as1=AS2_OBJ,
|
2023-03-08 21:10:41 +00:00
|
|
|
source_protocol='activitypub',
|
|
|
|
# check that it reused our original Object
|
|
|
|
status='in progress')
|
|
|
|
|
|
|
|
@patch('requests.get')
|
|
|
|
def test_signed_get_redirects_manually_with_new_sig_headers(self, mock_get):
|
|
|
|
mock_get.side_effect = [
|
|
|
|
requests_response(status=302, redirected_url='http://second',
|
|
|
|
allow_redirects=False),
|
|
|
|
requests_response(status=200, allow_redirects=False),
|
|
|
|
]
|
2023-03-20 21:28:14 +00:00
|
|
|
resp = activitypub.signed_get('https://first')
|
2023-03-08 21:10:41 +00:00
|
|
|
|
|
|
|
first = mock_get.call_args_list[0][1]
|
|
|
|
second = mock_get.call_args_list[1][1]
|
|
|
|
self.assertNotEqual(first['headers'], second['headers'])
|
|
|
|
self.assertNotEqual(
|
|
|
|
first['auth'].header_signer.sign(first['headers'], method='GET', path='/'),
|
|
|
|
second['auth'].header_signer.sign(second['headers'], method='GET', path='/'))
|
|
|
|
|
|
|
|
@patch('requests.post')
|
|
|
|
def test_signed_post_ignores_redirect(self, mock_post):
|
|
|
|
mock_post.side_effect = [
|
|
|
|
requests_response(status=302, redirected_url='http://second',
|
|
|
|
allow_redirects=False),
|
|
|
|
]
|
|
|
|
|
2023-03-20 21:28:14 +00:00
|
|
|
resp = activitypub.signed_post('https://first')
|
2023-03-08 21:10:41 +00:00
|
|
|
mock_post.assert_called_once()
|
|
|
|
self.assertEqual(302, resp.status_code)
|
|
|
|
|
|
|
|
@patch('requests.get')
|
|
|
|
def test_fetch_direct(self, mock_get):
|
|
|
|
mock_get.return_value = AS2
|
2023-04-03 14:53:15 +00:00
|
|
|
obj = Object(id='http://orig')
|
|
|
|
ActivityPub.fetch(obj)
|
|
|
|
self.assertEqual(AS2_OBJ, obj.as2)
|
2023-04-02 02:13:51 +00:00
|
|
|
|
2023-03-08 21:10:41 +00:00
|
|
|
mock_get.assert_has_calls((
|
|
|
|
self.as2_req('http://orig'),
|
|
|
|
))
|
|
|
|
|
|
|
|
@patch('requests.get')
|
|
|
|
def test_fetch_via_html(self, mock_get):
|
|
|
|
mock_get.side_effect = [HTML_WITH_AS2, AS2]
|
2023-04-03 14:53:15 +00:00
|
|
|
obj = Object(id='http://orig')
|
|
|
|
ActivityPub.fetch(obj)
|
|
|
|
self.assertEqual(AS2_OBJ, obj.as2)
|
2023-04-02 02:13:51 +00:00
|
|
|
|
2023-03-08 21:10:41 +00:00
|
|
|
mock_get.assert_has_calls((
|
|
|
|
self.as2_req('http://orig'),
|
|
|
|
self.as2_req('http://as2', headers=common.as2.CONNEG_HEADERS),
|
|
|
|
))
|
|
|
|
|
|
|
|
@patch('requests.get')
|
|
|
|
def test_fetch_only_html(self, mock_get):
|
|
|
|
mock_get.return_value = HTML
|
|
|
|
with self.assertRaises(BadGateway):
|
2023-04-03 14:53:15 +00:00
|
|
|
ActivityPub.fetch(Object(id='http://orig'))
|
2023-03-08 21:10:41 +00:00
|
|
|
|
|
|
|
@patch('requests.get')
|
|
|
|
def test_fetch_not_acceptable(self, mock_get):
|
|
|
|
mock_get.return_value=NOT_ACCEPTABLE
|
|
|
|
with self.assertRaises(BadGateway):
|
2023-04-03 14:53:15 +00:00
|
|
|
ActivityPub.fetch(Object(id='http://orig'))
|
2023-03-08 21:10:41 +00:00
|
|
|
|
|
|
|
@patch('requests.get')
|
|
|
|
def test_fetch_ssl_error(self, mock_get):
|
|
|
|
mock_get.side_effect = requests.exceptions.SSLError
|
|
|
|
with self.assertRaises(BadGateway):
|
2023-04-03 14:53:15 +00:00
|
|
|
ActivityPub.fetch(Object(id='http://orig'))
|
2023-03-08 21:10:41 +00:00
|
|
|
|
|
|
|
@patch('requests.get')
|
2023-03-23 03:49:28 +00:00
|
|
|
def test_fetch_no_content(self, mock_get):
|
2023-03-08 21:10:41 +00:00
|
|
|
mock_get.return_value = self.as2_resp('')
|
|
|
|
|
|
|
|
with self.assertRaises(BadGateway):
|
2023-04-03 14:53:15 +00:00
|
|
|
ActivityPub.fetch(Object(id='http://the/id'))
|
2023-03-08 21:10:41 +00:00
|
|
|
|
|
|
|
mock_get.assert_has_calls([self.as2_req('http://the/id')])
|
|
|
|
|
|
|
|
@patch('requests.get')
|
2023-03-23 03:49:28 +00:00
|
|
|
def test_fetch_not_json(self, mock_get):
|
2023-03-08 21:10:41 +00:00
|
|
|
mock_get.return_value = self.as2_resp('XYZ not JSON')
|
|
|
|
|
|
|
|
with self.assertRaises(BadGateway):
|
2023-04-03 14:53:15 +00:00
|
|
|
ActivityPub.fetch(Object(id='http://the/id'))
|
2023-03-08 21:10:41 +00:00
|
|
|
|
|
|
|
mock_get.assert_has_calls([self.as2_req('http://the/id')])
|
2023-04-01 19:46:31 +00:00
|
|
|
|
2023-05-24 04:30:57 +00:00
|
|
|
@skip
|
|
|
|
def test_serve(self):
|
|
|
|
obj = Object(id='http://orig', as2=LIKE)
|
|
|
|
self.assertEqual((LIKE_WRAPPED, {'Content-Type': 'application/activity+json'}),
|
|
|
|
ActivityPub.serve(obj))
|
|
|
|
|
2023-04-01 19:46:31 +00:00
|
|
|
def test_postprocess_as2_idempotent(self):
|
|
|
|
g.user = self.make_user('foo.com')
|
|
|
|
|
|
|
|
for obj in (ACTOR, REPLY_OBJECT, REPLY_OBJECT_WRAPPED, REPLY,
|
|
|
|
NOTE_OBJECT, NOTE, MENTION_OBJECT, MENTION, LIKE,
|
2023-04-02 02:13:51 +00:00
|
|
|
LIKE_WRAPPED, REPOST, FOLLOW, FOLLOW_WRAPPED, ACCEPT,
|
|
|
|
UNDO_FOLLOW_WRAPPED, DELETE, UPDATE_NOTE,
|
2023-04-01 19:46:31 +00:00
|
|
|
# TODO: these currently fail
|
|
|
|
# LIKE_WITH_ACTOR, REPOST_FULL, FOLLOW_WITH_ACTOR,
|
2023-04-02 02:13:51 +00:00
|
|
|
# FOLLOW_WRAPPED_WITH_ACTOR, FOLLOW_WITH_OBJECT, UPDATE_PERSON,
|
2023-04-01 19:46:31 +00:00
|
|
|
):
|
|
|
|
with self.subTest(obj=obj):
|
|
|
|
obj = copy.deepcopy(obj)
|
|
|
|
self.assert_equals(
|
|
|
|
activitypub.postprocess_as2(obj),
|
|
|
|
activitypub.postprocess_as2(activitypub.postprocess_as2(obj)),
|
|
|
|
ignore=['to'])
|