2017-08-15 14:39:22 +00:00
|
|
|
# coding=utf-8
|
|
|
|
"""Unit tests for webmention.py.
|
|
|
|
|
|
|
|
TODO: test error handling
|
|
|
|
"""
|
2017-08-19 19:36:36 +00:00
|
|
|
from __future__ import unicode_literals
|
2017-08-15 14:39:22 +00:00
|
|
|
import copy
|
|
|
|
import json
|
2017-09-03 22:26:41 +00:00
|
|
|
import logging
|
2017-08-15 14:39:22 +00:00
|
|
|
import urllib
|
2017-08-23 15:14:51 +00:00
|
|
|
import urllib2
|
2017-08-15 14:39:22 +00:00
|
|
|
|
2017-08-24 06:24:47 +00:00
|
|
|
from django_salmon import magicsigs, utils
|
2017-08-23 15:14:51 +00:00
|
|
|
import feedparser
|
2017-09-03 22:26:41 +00:00
|
|
|
from granary import atom, microformats2
|
2017-09-19 14:15:38 +00:00
|
|
|
from httpsig.sign import HeaderSigner
|
2017-09-03 22:26:41 +00:00
|
|
|
import mf2py
|
2017-08-15 14:39:22 +00:00
|
|
|
import mock
|
2017-08-23 15:14:51 +00:00
|
|
|
from mock import call
|
|
|
|
from oauth_dropins.webutil import util
|
2017-08-24 14:41:46 +00:00
|
|
|
from oauth_dropins.webutil.testutil import requests_response
|
2017-08-15 14:39:22 +00:00
|
|
|
import requests
|
|
|
|
|
|
|
|
import activitypub
|
|
|
|
import common
|
2017-10-10 00:29:50 +00:00
|
|
|
from models import MagicKey, Response
|
2017-08-24 06:24:47 +00:00
|
|
|
import testutil
|
2017-08-15 14:39:22 +00:00
|
|
|
import webmention
|
|
|
|
from webmention import app
|
|
|
|
|
|
|
|
|
|
|
|
@mock.patch('requests.post')
|
|
|
|
@mock.patch('requests.get')
|
2017-08-24 06:24:47 +00:00
|
|
|
class WebmentionTest(testutil.TestCase):
|
2017-08-23 15:14:51 +00:00
|
|
|
|
|
|
|
def setUp(self):
|
2017-08-24 06:24:47 +00:00
|
|
|
super(WebmentionTest, self).setUp()
|
2017-09-02 03:49:00 +00:00
|
|
|
self.orig = requests_response("""\
|
|
|
|
<html>
|
|
|
|
<meta>
|
|
|
|
<link href='http://orig/atom' rel='alternate' type='application/atom+xml'>
|
|
|
|
</meta>
|
|
|
|
</html>
|
2017-09-03 22:26:41 +00:00
|
|
|
""", url='http://orig/post', content_type='text/html; charset=utf-8')
|
2017-09-02 03:49:00 +00:00
|
|
|
|
2017-09-03 22:26:41 +00:00
|
|
|
self.reply_html = """\
|
2017-08-23 15:14:51 +00:00
|
|
|
<html>
|
|
|
|
<body>
|
2017-08-15 14:39:22 +00:00
|
|
|
<div class="h-entry">
|
2017-08-23 15:14:51 +00:00
|
|
|
<a class="u-url" href="http://a/reply"></a>
|
2017-10-02 14:53:06 +00:00
|
|
|
<p class="e-content p-name">
|
2017-08-15 14:39:22 +00:00
|
|
|
<a class="u-in-reply-to" href="http://orig/post">foo ☕ bar</a>
|
2017-10-02 04:43:01 +00:00
|
|
|
<a href="https://fed.brid.gy/"></a>
|
2017-08-15 14:39:22 +00:00
|
|
|
</p>
|
2017-10-02 14:53:06 +00:00
|
|
|
<a class="p-author h-card" href="http://orig">Ms. ☕ Baz</a>
|
2017-08-15 14:39:22 +00:00
|
|
|
</div>
|
2017-08-23 15:14:51 +00:00
|
|
|
</body>
|
|
|
|
</html>
|
2017-09-03 22:26:41 +00:00
|
|
|
"""
|
|
|
|
self.reply = requests_response(
|
|
|
|
self.reply_html, content_type='text/html; charset=utf-8')
|
|
|
|
mf2 = mf2py.parse(self.reply_html, url='http://a/reply')
|
|
|
|
self.reply_obj = microformats2.json_to_object(mf2['items'][0])
|
2017-08-15 14:39:22 +00:00
|
|
|
|
2017-08-24 14:41:46 +00:00
|
|
|
article = requests_response({
|
2017-08-15 14:39:22 +00:00
|
|
|
'@context': ['https://www.w3.org/ns/activitystreams'],
|
|
|
|
'type': 'Article',
|
2017-09-12 14:31:18 +00:00
|
|
|
'content': 'Lots of ☕ words...',
|
2017-08-26 22:20:54 +00:00
|
|
|
'actor': {
|
|
|
|
'url': 'http://orig/author',
|
|
|
|
},
|
2017-08-24 14:41:46 +00:00
|
|
|
})
|
|
|
|
actor = requests_response({
|
2017-08-15 14:39:22 +00:00
|
|
|
'objectType' : 'person',
|
2017-09-12 14:31:18 +00:00
|
|
|
'displayName': 'Mrs. ☕ Foo',
|
2017-08-15 14:39:22 +00:00
|
|
|
'url': 'https://foo.com/about-me',
|
|
|
|
'inbox': 'https://foo.com/inbox',
|
2017-08-24 14:41:46 +00:00
|
|
|
})
|
2017-10-10 01:12:17 +00:00
|
|
|
self.activitypub_gets = [self.reply, article, actor]
|
2017-08-15 14:39:22 +00:00
|
|
|
|
2017-10-10 01:12:17 +00:00
|
|
|
self.as2_create = {
|
2017-09-20 15:04:28 +00:00
|
|
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
|
|
'type': 'Create',
|
|
|
|
'object': {
|
2017-09-26 14:54:37 +00:00
|
|
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
2017-09-20 15:04:28 +00:00
|
|
|
'type': 'Note',
|
2017-10-02 04:43:01 +00:00
|
|
|
'id': 'http://a/reply',
|
2017-09-20 15:04:28 +00:00
|
|
|
'url': 'http://a/reply',
|
|
|
|
'displayName': 'foo ☕ bar',
|
2017-10-02 04:43:01 +00:00
|
|
|
'content': ' <a class="u-in-reply-to" href="http://orig/post">foo ☕ bar</a> <a href="https://fed.brid.gy/"></a> ',
|
2017-09-20 15:04:28 +00:00
|
|
|
'inReplyTo': 'http://orig/post',
|
|
|
|
'cc': [
|
2017-09-28 14:25:21 +00:00
|
|
|
common.AS2_PUBLIC_AUDIENCE,
|
2017-10-02 14:53:06 +00:00
|
|
|
'http://orig/post',
|
2017-09-20 15:04:28 +00:00
|
|
|
],
|
2017-10-02 14:53:06 +00:00
|
|
|
'attributedTo': [{
|
|
|
|
'type': 'Person',
|
|
|
|
'url': 'http://orig',
|
|
|
|
'preferredUsername': 'me',
|
|
|
|
'displayName': 'Ms. ☕ Baz',
|
|
|
|
}],
|
2017-09-20 15:04:28 +00:00
|
|
|
},
|
2017-10-10 00:29:50 +00:00
|
|
|
}
|
2017-10-10 01:12:17 +00:00
|
|
|
self.as2_update = copy.deepcopy(self.as2_create)
|
|
|
|
self.as2_update['type'] = 'Update'
|
|
|
|
|
|
|
|
def test_activitypub_create(self, mock_get, mock_post):
|
|
|
|
mock_get.side_effect = self.activitypub_gets
|
|
|
|
mock_post.return_value = requests_response('abc xyz')
|
|
|
|
|
|
|
|
got = app.get_response(
|
|
|
|
'/webmention', method='POST', body=urllib.urlencode({
|
|
|
|
'source': 'http://a/reply',
|
|
|
|
'target': 'https://fed.brid.gy/',
|
|
|
|
}))
|
|
|
|
self.assertEquals(200, got.status_int)
|
|
|
|
|
|
|
|
mock_get.assert_has_calls((
|
|
|
|
call('http://a/reply', headers=common.HEADERS, timeout=util.HTTP_TIMEOUT),
|
|
|
|
call('http://orig/post', headers=activitypub.CONNEG_HEADER,
|
|
|
|
timeout=util.HTTP_TIMEOUT),
|
|
|
|
call('http://orig/author', headers=activitypub.CONNEG_HEADER,
|
|
|
|
timeout=util.HTTP_TIMEOUT),))
|
|
|
|
|
2017-10-10 00:29:50 +00:00
|
|
|
args, kwargs = mock_post.call_args
|
|
|
|
self.assertEqual(('https://foo.com/inbox',), args)
|
2017-10-10 01:12:17 +00:00
|
|
|
self.assertEqual(self.as2_create, kwargs['json'])
|
2017-08-15 14:39:22 +00:00
|
|
|
|
2017-09-19 14:15:38 +00:00
|
|
|
headers = kwargs['headers']
|
|
|
|
self.assertEqual(activitypub.CONTENT_TYPE_AS, headers['Content-Type'])
|
|
|
|
|
|
|
|
expected_key = MagicKey.get_by_id('a')
|
|
|
|
rsa_key = kwargs['auth'].header_signer._rsa._key
|
|
|
|
self.assertEqual(expected_key.private_pem(), rsa_key.exportKey())
|
2017-08-23 15:14:51 +00:00
|
|
|
|
2017-10-10 00:29:50 +00:00
|
|
|
resp = Response.get_by_id('http://a/reply http://orig/post')
|
|
|
|
self.assertEqual('out', resp.direction)
|
|
|
|
self.assertEqual('activitypub', resp.protocol)
|
|
|
|
self.assertEqual('complete', resp.status)
|
|
|
|
|
|
|
|
# TODO: if i do this, maybe switch to separate HttpRequest model and
|
|
|
|
# foreign key
|
2017-10-10 01:12:17 +00:00
|
|
|
# self.assertEqual([self.as2_create], resp.request_statuses)
|
|
|
|
# self.assertEqual([self.as2_create], resp.requests)
|
2017-10-10 00:29:50 +00:00
|
|
|
# self.assertEqual(['abc xyz'], resp.responses)
|
|
|
|
|
2017-10-10 01:12:17 +00:00
|
|
|
def test_activitypub_update(self, mock_get, mock_post):
|
|
|
|
Response(id='http://a/reply http://orig/post', status='complete').put()
|
|
|
|
|
|
|
|
mock_get.side_effect = self.activitypub_gets
|
|
|
|
mock_post.return_value = requests_response('abc xyz')
|
|
|
|
|
|
|
|
got = app.get_response(
|
|
|
|
'/webmention', method='POST', body=urllib.urlencode({
|
|
|
|
'source': 'http://a/reply',
|
|
|
|
'target': 'https://fed.brid.gy/',
|
|
|
|
}))
|
|
|
|
self.assertEquals(200, got.status_int)
|
|
|
|
|
|
|
|
args, kwargs = mock_post.call_args
|
|
|
|
self.assertEqual(('https://foo.com/inbox',), args)
|
|
|
|
self.assertEqual(self.as2_update, kwargs['json'])
|
|
|
|
|
2017-09-02 03:49:00 +00:00
|
|
|
def test_salmon(self, mock_get, mock_post):
|
2017-09-03 22:26:41 +00:00
|
|
|
orig_atom = requests_response("""\
|
2017-08-23 15:14:51 +00:00
|
|
|
<?xml version="1.0"?>
|
|
|
|
<entry xmlns="http://www.w3.org/2005/Atom">
|
|
|
|
<id>tag:fed.brid.gy,2017-08-22:orig-post</id>
|
|
|
|
<link rel="salmon" href="http://orig/salmon"/>
|
|
|
|
<content type="html">baz ☕ baj</content>
|
|
|
|
</entry>
|
2017-08-24 14:41:46 +00:00
|
|
|
""")
|
2017-09-03 22:26:41 +00:00
|
|
|
mock_get.side_effect = [self.reply, self.orig, orig_atom]
|
2017-08-23 15:14:51 +00:00
|
|
|
|
|
|
|
got = app.get_response(
|
|
|
|
'/webmention', method='POST', body=urllib.urlencode({
|
|
|
|
'source': 'http://a/reply',
|
|
|
|
'target': 'http://orig/post',
|
|
|
|
}))
|
|
|
|
self.assertEquals(200, got.status_int)
|
|
|
|
|
|
|
|
mock_get.assert_has_calls((
|
|
|
|
call('http://a/reply', headers=common.HEADERS, timeout=util.HTTP_TIMEOUT),
|
|
|
|
call('http://orig/post', headers=activitypub.CONNEG_HEADER,
|
|
|
|
timeout=util.HTTP_TIMEOUT),
|
|
|
|
call('http://orig/atom', headers=common.HEADERS, timeout=util.HTTP_TIMEOUT),
|
|
|
|
))
|
|
|
|
|
|
|
|
args, kwargs = mock_post.call_args
|
|
|
|
self.assertEqual(('http://orig/salmon',), args)
|
|
|
|
self.assertEqual(common.MAGIC_ENVELOPE_CONTENT_TYPE,
|
|
|
|
kwargs['headers']['Content-Type'])
|
|
|
|
|
2017-09-03 22:26:41 +00:00
|
|
|
env = utils.parse_magic_envelope(kwargs['data'])
|
|
|
|
self.reply_obj['inReplyTo'][0]['id'] = 'tag:fed.brid.gy,2017-08-22:orig-post'
|
|
|
|
reply_atom = atom.activity_to_atom(
|
|
|
|
{'object': self.reply_obj}, xml_base='http://a/reply')
|
2017-09-12 15:16:54 +00:00
|
|
|
key = MagicKey.get_by_id('a')
|
2017-09-03 22:26:41 +00:00
|
|
|
assert magicsigs.verify(None, reply_atom, env['sig'], key=key)
|
2017-08-23 15:14:51 +00:00
|
|
|
|
2017-09-03 22:26:41 +00:00
|
|
|
data = utils.decode(env['data'])
|
2017-08-26 22:20:54 +00:00
|
|
|
parsed = feedparser.parse(data)
|
2017-08-23 15:14:51 +00:00
|
|
|
entry = parsed.entries[0]
|
|
|
|
|
2017-08-26 22:20:54 +00:00
|
|
|
self.assertEquals('http://a/reply', entry['id'])
|
2017-08-23 15:14:51 +00:00
|
|
|
self.assertIn({
|
|
|
|
'rel': 'alternate',
|
|
|
|
'href': 'http://a/reply',
|
|
|
|
'type': 'text/html',
|
2017-08-26 22:20:54 +00:00
|
|
|
}, entry['links'])
|
2017-08-23 15:14:51 +00:00
|
|
|
self.assertEquals({
|
|
|
|
'type': 'text/html',
|
|
|
|
'href': 'http://orig/post',
|
|
|
|
'ref': 'tag:fed.brid.gy,2017-08-22:orig-post'
|
|
|
|
}, entry['thr_in-reply-to'])
|
|
|
|
self.assertEquals(
|
2017-10-02 04:43:01 +00:00
|
|
|
'<a class="u-in-reply-to" href="http://orig/post">foo ☕ bar</a> <a href="https://fed.brid.gy/"></a>',
|
2017-08-23 15:14:51 +00:00
|
|
|
entry.content[0]['value'])
|
2017-09-02 03:49:00 +00:00
|
|
|
|
2017-10-10 00:29:50 +00:00
|
|
|
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)
|
|
|
|
|
2017-09-02 03:49:00 +00:00
|
|
|
def test_salmon_get_salmon_from_webfinger(self, mock_get, mock_post):
|
2017-09-03 22:26:41 +00:00
|
|
|
orig_atom = requests_response("""\
|
2017-09-02 03:49:00 +00:00
|
|
|
<?xml version="1.0"?>
|
|
|
|
<entry xmlns="http://www.w3.org/2005/Atom">
|
|
|
|
<author>
|
|
|
|
<name>ryan</name>
|
|
|
|
<email>ryan@orig</email>
|
|
|
|
</author>
|
|
|
|
<id>tag:fed.brid.gy,2017-08-22:orig-post</id>
|
|
|
|
</entry>
|
|
|
|
""")
|
|
|
|
webfinger = requests_response({
|
|
|
|
'subject': 'acct:ryan@orig',
|
|
|
|
'links': [{
|
|
|
|
'rel': 'salmon',
|
|
|
|
'href': 'http://orig/@ryan/salmon',
|
|
|
|
}],
|
|
|
|
})
|
2017-09-03 22:26:41 +00:00
|
|
|
mock_get.side_effect = [self.reply, self.orig, orig_atom, webfinger]
|
2017-09-02 03:49:00 +00:00
|
|
|
|
|
|
|
got = app.get_response('/webmention', method='POST', body=urllib.urlencode({
|
|
|
|
'source': 'http://a/reply',
|
|
|
|
'target': 'http://orig/post',
|
|
|
|
}))
|
|
|
|
self.assertEquals(200, got.status_int)
|
|
|
|
|
|
|
|
mock_get.assert_any_call(
|
2017-10-04 14:09:02 +00:00
|
|
|
'http://orig/.well-known/webfinger?resource=acct:ryan@orig',
|
|
|
|
headers=common.HEADERS, timeout=util.HTTP_TIMEOUT, verify=False)
|
2017-09-02 03:49:00 +00:00
|
|
|
self.assertEqual(('http://orig/@ryan/salmon',), mock_post.call_args[0])
|
2017-09-14 13:52:18 +00:00
|
|
|
|
|
|
|
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)
|
2017-10-10 00:29:50 +00:00
|
|
|
|
|
|
|
self.assertIsNone(Response.get_by_id('http://a/reply http://orig/post'))
|
|
|
|
|