add /render logic to /convert/...

pull/521/head
Ryan Barrett 2023-05-24 16:00:41 -07:00
rodzic df35ce16cc
commit 6d976854dc
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
2 zmienionych plików z 210 dodań i 26 usunięć

Wyświetl plik

@ -9,13 +9,15 @@ import logging
import re
import urllib.parse
from flask import request
from flask import redirect, request
from granary import as1
from oauth_dropins.webutil import flask_util, util
from oauth_dropins.webutil.flask_util import error
from activitypub import ActivityPub
from common import CACHE_TIME
from flask_app import app, cache
from models import Object
from protocol import protocols
from webmention import Webmention
@ -29,15 +31,19 @@ DESTS = frozenset((
))
@app.get(f'/convert/<any({",".join(SOURCES)}):src>/<any({",".join(DESTS)}):dest>/<path:url>')
@app.get(f'/convert/<any({",".join(SOURCES)}):src>/<any({",".join(DESTS)}):dest>/<path:_>')
@flask_util.cached(cache, CACHE_TIME, headers=['Accept'])
def convert(src, dest, url):
def convert(src, dest, _):
"""Converts data from one protocol to another and serves it.
Fetches the source data if it's not already stored.
"""
if request.args:
url += '?' + urllib.parse.urlencode(request.args)
# don't use urllib.parse.urlencode(request.args) because that doesn't
# guarantee us the same query param string as in the original URL, and we
# want exactly the same thing since we're looking up the URL's Object by id
path_prefix = f'convert/{src}/{dest}/'
url = request.url.removeprefix(request.root_url).removeprefix(path_prefix)
# some browsers collapse repeated /s in the path down to a single slash.
# if that happened to this URL, expand it back to two /s.
url = re.sub(r'^(https?:/)([^/])', r'\1/\2', url)
@ -45,5 +51,25 @@ def convert(src, dest, url):
if not util.is_web(url):
error(f'Expected fully qualified URL; got {url}')
# load, and maybe fetch. if it's a post/update, redirect to inner object.
obj = protocols[src].load(url)
if not obj.as1:
error(f'Stored object for {id} has no data', status=404)
type = as1.object_type(obj.as1)
if type in ('post', 'update', 'delete'):
obj_id = as1.get_object(obj.as1).get('id')
if obj_id:
# TODO: protocols[src].load() this instead?
obj_obj = Object.get_by_id(obj_id)
if (obj_obj and obj_obj.as1 and
not obj_obj.as1.keys() <= set(['id', 'url', 'objectType'])):
logger.info(f'{type} activity, redirecting to Object {obj_id}')
return redirect(f'/{path_prefix}{obj_id}', code=301)
# don't serve deletes or deleted objects
if obj.deleted or type == 'delete':
return '', 410
# convert and serve
return protocols[dest].serve(obj)

Wyświetl plik

@ -1,7 +1,11 @@
"""Unit tests for convert.py.
"""
import copy
from unittest.mock import patch
from granary import as2
from granary.tests.test_as1 import ACTOR, COMMENT, DELETE_OF_ID, UPDATE
from models import Object
from oauth_dropins.webutil.testutil import requests_response
import requests
@ -13,38 +17,192 @@ from .test_redirect import (
)
from . import testutil
EXPECTED_HTML = """\
<!DOCTYPE html>
<html>
<head><meta charset="utf-8">
<meta http-equiv="refresh" content="0;url=https://fake.com/123456"></head>
<body class="">
<article class="h-entry">
<span class="p-uid">tag:fake.com:123456</span>
<time class="dt-published" datetime="2012-12-05T00:58:26+00:00">2012-12-05T00:58:26+00:00</time>
<a class="u-url" href="https://fake.com/123456">fake.com/123456</a>
<div class="e-content p-name">
A reply
</div>
<a class="u-in-reply-to" href="https://fake.com/123"></a>
</article>
</body>
</html>
"""
EXPECTED_AUTHOR_HTML = """\
<!DOCTYPE html>
<html>
<head><meta charset="utf-8">
<meta http-equiv="refresh" content="0;url=https://fake.com/123456"></head>
<body class="">
<article class="h-entry">
<span class="p-uid">tag:fake.com:123456</span>
<time class="dt-published" datetime="2012-12-05T00:58:26+00:00">2012-12-05T00:58:26+00:00</time>
<span class="p-author h-card">
<data class="p-uid" value="tag:fake.com:444"></data>
<a class="p-name u-url" href="https://plus.google.com/bob">Bob</a>
<img class="u-photo" src="https://bob/picture" alt="" />
</span>
<a class="u-url" href="https://fake.com/123456">fake.com/123456</a>
<div class="e-content p-name">
A reply
</div>
<a class="u-in-reply-to" href="https://fake.com/123"></a>
</article>
</body>
</html>
"""
@patch('requests.get')
class ConvertTest(testutil.TestCase):
def test_unknown_source(self, _):
got = self.client.get('/convert/nope/webmention/http://foo')
self.assertEqual(404, got.status_code)
def test_unknown_source(self):
resp = self.client.get('/convert/nope/webmention/http://foo')
self.assertEqual(404, resp.status_code)
def test_unknown_dest(self, _):
got = self.client.get('/convert/activitypub/nope/http://foo')
self.assertEqual(404, got.status_code)
def test_unknown_dest(self):
resp = self.client.get('/convert/activitypub/nope/http://foo')
self.assertEqual(404, resp.status_code)
def test_missing_url(self, _):
got = self.client.get('/convert/activitypub/webmention/')
self.assertEqual(404, got.status_code)
def test_missing_url(self):
resp = self.client.get('/convert/activitypub/webmention/')
self.assertEqual(404, resp.status_code)
def test_url_not_web(self, _):
got = self.client.get('/convert/activitypub/webmention/git+ssh://foo/bar')
self.assertEqual(400, got.status_code)
def test_url_not_web(self):
resp = self.client.get('/convert/activitypub/webmention/git+ssh://foo/bar')
self.assertEqual(400, resp.status_code)
def test_activitypub_to_web(self, mock_get):
mock_get.return_value = self.as2_resp(REPOST_AS2)
def test_activitypub_to_web_object(self):
url = 'https://user.com/bar?baz=baj&biff'
with self.request_context:
Object(id=url, our_as1=COMMENT).put()
got = self.client.get('/convert/activitypub/webmention/https://user.com/bar?baz=baj&biff')
self.assertEqual(200, got.status_code)
self.assertEqual(CONTENT_TYPE_HTML, got.content_type)
resp = self.client.get('/convert/activitypub/webmention/https://user.com/bar?baz=baj&biff')
self.assertEqual(200, resp.status_code)
self.assert_multiline_equals(EXPECTED_HTML, resp.get_data(as_text=True),
ignore_blanks=True)
mock_get.assert_has_calls((self.as2_req('https://user.com/bar?baz=baj&biff='),))
def test_activitypub_to_web_object_empty(self):
with self.request_context:
Object(id='http://foo').put()
resp = self.client.get('/convert/activitypub/webmention/http://foo')
self.assertEqual(404, resp.status_code)
@patch('requests.get')
def test_activitypub_to_web_fetch(self, mock_get):
mock_get.return_value = self.as2_resp(as2.from_as1(COMMENT))
url = 'https://user.com/bar?baz=baj&biff'
resp = self.client.get(f'/convert/activitypub/webmention/{url}')
self.assertEqual(200, resp.status_code)
self.assertEqual(CONTENT_TYPE_HTML, resp.content_type)
self.assert_multiline_equals(EXPECTED_HTML, resp.get_data(as_text=True),
ignore_blanks=True)
mock_get.assert_has_calls((self.as2_req(url),))
@patch('requests.get')
def test_activitypub_to_web_fetch_fails(self, mock_get):
mock_get.side_effect = [requests_response('', status=405)]
got = self.client.get('/convert/activitypub/webmention/http://foo')
self.assertEqual(502, got.status_code)
resp = self.client.get('/convert/activitypub/webmention/http://foo')
self.assertEqual(502, resp.status_code)
mock_get.assert_has_calls((self.as2_req('http://foo'),))
def test_activitypub_to_web_with_author(self):
with self.request_context:
Object(id='http://foo', our_as1={**COMMENT, 'author': 'http://bar'},
source_protocol='activitypub').put()
Object(id='http://bar', our_as1=ACTOR,
source_protocol='activitypub').put()
resp = self.client.get('/convert/activitypub/webmention/http://foo')
self.assertEqual(200, resp.status_code)
self.assert_multiline_equals(EXPECTED_AUTHOR_HTML, resp.get_data(as_text=True),
ignore_blanks=True)
def test_activitypub_to_web_no_url(self):
comment = copy.deepcopy(COMMENT)
del comment['url']
with self.request_context:
Object(id='http://foo', our_as1=comment).put()
resp = self.client.get('/convert/activitypub/webmention/http://foo')
self.assertEqual(200, resp.status_code)
expected = EXPECTED_HTML.replace(
'\n<meta http-equiv="refresh" content="0;url=https://fake.com/123456">', ''
).replace('<a class="u-url" href="https://fake.com/123456">fake.com/123456</a>', '')
self.assert_multiline_equals(expected, resp.get_data(as_text=True),
ignore_blanks=True)
def test_activitypub_to_web_deleted_object(self):
with self.request_context:
Object(id='http://foo', as2={'content': 'foo'}, deleted=True).put()
resp = self.client.get('/convert/activitypub/webmention/http://foo')
self.assertEqual(410, resp.status_code)
def test_activitypub_to_web_delete_activity(self):
with self.request_context:
Object(id='http://foo', our_as1=DELETE_OF_ID).put()
resp = self.client.get('/convert/activitypub/webmention/http://foo')
self.assertEqual(410, resp.status_code)
def test_activitypub_to_web_update_inner_obj_exists_redirect(self):
with self.request_context:
# UPDATE's object field is a full object
Object(id='http://foo', our_as1=UPDATE).put()
Object(id=UPDATE['object']['id'], as2={'content': 'foo'}).put()
resp = self.client.get('/convert/activitypub/webmention/http://foo')
self.assertEqual(301, resp.status_code)
self.assertEqual(f'/convert/activitypub/webmention/tag:fake.com:123456',
resp.headers['Location'])
def test_activitypub_to_web_delete_inner_obj_exists_redirect(self):
with self.request_context:
# DELETE_OF_ID's object field is a bare string id
Object(id='http://foo', our_as1=DELETE_OF_ID).put()
Object(id=DELETE_OF_ID['object'], as2={'content': 'foo'}).put()
resp = self.client.get('/convert/activitypub/webmention/http://foo')
self.assertEqual(301, resp.status_code)
self.assertEqual(f'/convert/activitypub/webmention/tag:fake.com:123456',
resp.headers['Location'])
def test_activitypub_to_web_update_no_inner_obj_serve_as_is(self):
with self.request_context:
# UPDATE's object field is a full object
Object(id='http://foo', our_as1=UPDATE).put()
resp = self.client.get('/convert/activitypub/webmention/http://foo')
self.assertEqual(200, resp.status_code)
self.assert_multiline_in("""\
<div class="e-content p-name">
A reply
</div>
<a class="u-in-reply-to" href="https://fake.com/123"></a>
""", resp.get_data(as_text=True), ignore_blanks=True)
def test_activitypub_to_web_update_inner_obj_too_minimal_serve_as_is(self):
with self.request_context:
# UPDATE's object field is a full object
Object(id='http://foo', our_as1=UPDATE).put()
Object(id=UPDATE['object']['id'], as2={'id': 'foo'}).put()
resp = self.client.get('/convert/activitypub/webmention/http://foo')
self.assertEqual(200, resp.status_code)
self.assert_multiline_in("""\
<div class="e-content p-name">
A reply
</div>
<a class="u-in-reply-to" href="https://fake.com/123"></a>
""", resp.get_data(as_text=True), ignore_blanks=True)