Mercurial > libervia-backend
view tests/unit/test_ap-gateway.py @ 3760:74f436e856ff
plugin pubsub cache: new `resync` argument to force resynchronisation in `synchronize`
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 13 May 2022 18:44:54 +0200 |
parents | 04ecc8eeb81a |
children | f31113777881 |
line wrap: on
line source
#!/usr/bin/env python3 # Libervia: an XMPP client # Copyright (C) 2009-2022 Jérôme Poisson (goffi@goffi.org) # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. from copy import deepcopy from unittest.mock import MagicMock, patch from urllib import parse import pytest from pytest_twisted import ensureDeferred as ed from twisted.words.protocols.jabber import jid from twisted.web.server import Request from wokkel import rsm, pubsub from sat.core import exceptions from sat.plugins import plugin_comp_ap_gateway from sat.plugins.plugin_comp_ap_gateway.http_server import HTTPServer from sat.tools.utils import xmpp_date from sat.tools import xml_tools TEST_BASE_URL = "https://example.org" TEST_USER = "test_user" TEST_AP_ACCOUNT = f"{TEST_USER}@example.org" AP_REQUESTS = { f"{TEST_BASE_URL}/.well-known/webfinger?" f"resource=acct:{parse.quote(TEST_AP_ACCOUNT)}": { "aliases": [ f"{TEST_BASE_URL}/@{TEST_USER}", f"{TEST_BASE_URL}/users/{TEST_USER}" ], "links": [ { "href": f"{TEST_BASE_URL}/users/{TEST_USER}", "rel": "self", "type": "application/activity+json" }, ], "subject": f"acct:{TEST_AP_ACCOUNT}" }, f"{TEST_BASE_URL}/users/{TEST_USER}": { "@context": [ "https://www.w3.org/ns/activitystreams", ], "endpoints": { "sharedInbox": f"{TEST_BASE_URL}/inbox" }, "followers": f"{TEST_BASE_URL}/users/{TEST_USER}/followers", "following": f"{TEST_BASE_URL}/users/{TEST_USER}/following", "id": f"{TEST_BASE_URL}/users/{TEST_USER}", "inbox": f"{TEST_BASE_URL}/users/{TEST_USER}/inbox", "name": "", "outbox": f"{TEST_BASE_URL}/users/{TEST_USER}/outbox", "preferredUsername": f"{TEST_USER}", "type": "Person", "url": f"{TEST_BASE_URL}/@{TEST_USER}" }, f"{TEST_BASE_URL}/users/{TEST_USER}/outbox": { "@context": "https://www.w3.org/ns/activitystreams", "first": f"{TEST_BASE_URL}/users/{TEST_USER}/outbox?page=true", "id": f"{TEST_BASE_URL}/users/{TEST_USER}/outbox", "last": f"{TEST_BASE_URL}/users/{TEST_USER}/outbox?page=true", "totalItems": 4, "type": "OrderedCollection" }, f"{TEST_BASE_URL}/users/{TEST_USER}/outbox?page=true": { "@context": [ "https://www.w3.org/ns/activitystreams", ], "id": f"{TEST_BASE_URL}/users/{TEST_USER}/outbox?page=true", "orderedItems": [ { "actor": f"{TEST_BASE_URL}/users/{TEST_USER}", "cc": [ f"{TEST_BASE_URL}/users/{TEST_USER}/followers", ], "id": f"{TEST_BASE_URL}/users/{TEST_USER}/statuses/1/activity", "object": { "attributedTo": f"{TEST_BASE_URL}/users/{TEST_USER}", "cc": [ f"{TEST_BASE_URL}/users/{TEST_USER}/followers", ], "content": "<p>test message 1</p>", "contentMap": { "en": "<p>test message 1</p>" }, "id": f"{TEST_BASE_URL}/users/{TEST_USER}/statuses/1", "inReplyTo": None, "published": "2021-12-16T17:28:03Z", "sensitive": False, "summary": None, "tag": [], "to": [ "https://www.w3.org/ns/activitystreams#Public" ], "type": "Note", "url": f"{TEST_BASE_URL}/@{TEST_USER}/1" }, "published": "2021-12-16T17:28:03Z", "to": [ "https://www.w3.org/ns/activitystreams#Public" ], "type": "Create" }, { "actor": f"{TEST_BASE_URL}/users/{TEST_USER}", "cc": [ f"{TEST_BASE_URL}/users/{TEST_USER}/followers", ], "id": f"{TEST_BASE_URL}/users/{TEST_USER}/statuses/2/activity", "object": { "attributedTo": f"{TEST_BASE_URL}/users/{TEST_USER}", "cc": [ f"{TEST_BASE_URL}/users/{TEST_USER}/followers", ], "content": "<p>test message 2</p>", "contentMap": { "en": "<p>test message 2</p>" }, "id": f"{TEST_BASE_URL}/users/{TEST_USER}/statuses/2", "inReplyTo": None, "published": "2021-12-16T17:27:03Z", "sensitive": False, "summary": None, "tag": [], "to": [ "https://www.w3.org/ns/activitystreams#Public" ], "type": "Note", "url": f"{TEST_BASE_URL}/@{TEST_USER}/2" }, "published": "2021-12-16T17:27:03Z", "to": [ "https://www.w3.org/ns/activitystreams#Public" ], "type": "Create" }, { "actor": f"{TEST_BASE_URL}/users/{TEST_USER}", "cc": [ f"{TEST_BASE_URL}/users/{TEST_USER}/followers", ], "id": f"{TEST_BASE_URL}/users/{TEST_USER}/statuses/3/activity", "object": { "attributedTo": f"{TEST_BASE_URL}/users/{TEST_USER}", "cc": [ f"{TEST_BASE_URL}/users/{TEST_USER}/followers", ], "content": "<p>test message 3</p>", "contentMap": { "en": "<p>test message 3</p>" }, "id": f"{TEST_BASE_URL}/users/{TEST_USER}/statuses/3", "inReplyTo": None, "published": "2021-12-16T17:26:03Z", "sensitive": False, "summary": None, "tag": [], "to": [ "https://www.w3.org/ns/activitystreams#Public" ], "type": "Note", "url": f"{TEST_BASE_URL}/@{TEST_USER}/3" }, "published": "2021-12-16T17:26:03Z", "to": [ "https://www.w3.org/ns/activitystreams#Public" ], "type": "Create" }, { "actor": f"{TEST_BASE_URL}/users/{TEST_USER}", "cc": [ f"{TEST_BASE_URL}/users/{TEST_USER}/followers", ], "id": f"{TEST_BASE_URL}/users/{TEST_USER}/statuses/4/activity", "object": { "attributedTo": f"{TEST_BASE_URL}/users/{TEST_USER}", "cc": [ f"{TEST_BASE_URL}/users/{TEST_USER}/followers", ], "content": "<p>test message 4</p>", "contentMap": { "en": "<p>test message 4</p>" }, "id": f"{TEST_BASE_URL}/users/{TEST_USER}/statuses/4", "inReplyTo": None, "published": "2021-12-16T17:25:03Z", "sensitive": False, "summary": None, "tag": [], "to": [ "https://www.w3.org/ns/activitystreams#Public" ], "type": "Note", "url": f"{TEST_BASE_URL}/@{TEST_USER}/4" }, "published": "2021-12-16T17:25:03Z", "to": [ "https://www.w3.org/ns/activitystreams#Public" ], "type": "Create" }, ], "partOf": f"{TEST_BASE_URL}/users/{TEST_USER}/outbox", "prev": None, "type": "OrderedCollectionPage" } } XMPP_ITEM_TPL = """ <item id='{id}' publisher='{publisher_jid}'> <entry xmlns='http://www.w3.org/2005/Atom' xml:lang='en'> <title type='xhtml'> <div xmlns='http://www.w3.org/1999/xhtml'> <p> XMPP item {id} </p> </div> </title> <title type='text'> XMPP item {id} </title> <author> <name> test_user </name> <uri> xmpp:{publisher_jid} </uri> </author> <updated> {updated} </updated> <published> {published} </published> <id> xmpp:{publisher_jid}?;node=urn%3Axmpp%3Amicroblog%3A0;item={id} </id> </entry> </item> """ ITEM_BASE_TS = 1643385499 XMPP_ITEMS = [ xml_tools.parse( "".join( l.strip() for l in XMPP_ITEM_TPL.format( id=i, publisher_jid="some_user@test.example", updated=xmpp_date(ITEM_BASE_TS + i * 60), published=xmpp_date(ITEM_BASE_TS + i * 60), ).split("\n") ), namespace=pubsub.NS_PUBSUB ) for i in range(1, 5) ] async def mock_ap_get(url): return deepcopy(AP_REQUESTS[url]) async def mock_treq_json(data): return dict(data) async def mock_getItems(*args, **kwargs): rsm_resp = rsm.RSMResponse( first=XMPP_ITEMS[0]["id"], last=XMPP_ITEMS[-1]["id"], index=0, count=len(XMPP_ITEMS) ) return XMPP_ITEMS, {"rsm": rsm_resp.toDict(), "complete": True} @pytest.fixture(scope="session") def ap_gateway(host): gateway = plugin_comp_ap_gateway.APGateway(host) gateway.initialised = True client = MagicMock() client.jid = jid.JID("ap.test.example") client.host = "test.example" gateway.client = client gateway.local_only = True gateway.public_url = "test.example" gateway.ap_path = '_ap' gateway.base_ap_url = parse.urljoin( f"https://{gateway.public_url}", f"{gateway.ap_path}/" ) gateway.server = HTTPServer(gateway) return gateway class TestActivityPubGateway: @ed async def test_jid_and_node_convert_to_ap_handle(self, ap_gateway): """JID and pubsub node are converted correctly to an AP actor handle""" get_account = ap_gateway.getAPAccountFromJidAndNode # local jid assert await get_account( jid_ = jid.JID("simple@test.example"), node = None ) == "simple@test.example" # non local jid assert await get_account( jid_ = jid.JID("simple@example.org"), node = None ) == "___simple.40example.2eorg@ap.test.example" # local jid with non microblog node assert await get_account( jid_ = jid.JID("simple@test.example"), node = "some_other_node" ) == "some_other_node---simple@test.example" # local pubsub node with patch.object(ap_gateway, "isPubsub") as isPubsub: isPubsub.return_value = True assert await get_account( jid_ = jid.JID("pubsub.test.example"), node = "some_node" ) == "some_node@pubsub.test.example" # non local pubsub node with patch.object(ap_gateway, "isPubsub") as isPubsub: isPubsub.return_value = True assert await get_account( jid_ = jid.JID("pubsub.example.org"), node = "some_node" ) == "___some_node.40pubsub.2eexample.2eorg@ap.test.example" @ed async def test_ap_handle_convert_to_jid_and_node(self, ap_gateway, monkeypatch): """AP actor handle convert correctly to JID and pubsub node""" get_jid_node = ap_gateway.getJIDAndNode # for following assertion, host is not a pubsub service with patch.object(ap_gateway, "isPubsub") as isPubsub: isPubsub.return_value = False # simple local jid assert await get_jid_node( "toto@test.example" ) == (jid.JID("toto@test.example"), None) # simple external jid ## with "local_only" set, it should raise an exception with pytest.raises(exceptions.PermissionError): await get_jid_node("toto@example.org") ## with "local_only" unset, it should work with monkeypatch.context() as m: m.setattr(ap_gateway, "local_only", False, raising=True) assert await get_jid_node( "toto@example.org" ) == (jid.JID("toto@example.org"), None) # explicit node assert await get_jid_node( "tata---toto@test.example" ) == (jid.JID("toto@test.example"), "tata") # for following assertion, host is a pubsub service with patch.object(ap_gateway, "isPubsub") as isPubsub: isPubsub.return_value = True # simple local node assert await get_jid_node( "toto@pubsub.test.example" ) == (jid.JID("pubsub.test.example"), "toto") # encoded local node assert await get_jid_node( "___urn.3axmpp.3amicroblog.3a0@pubsub.test.example" ) == (jid.JID("pubsub.test.example"), "urn:xmpp:microblog:0") @ed async def test_ap_to_pubsub_conversion(self, ap_gateway, monkeypatch): """AP requests are converted to pubsub""" monkeypatch.setattr(plugin_comp_ap_gateway.treq, "get", mock_ap_get) monkeypatch.setattr(plugin_comp_ap_gateway.treq, "json_content", mock_treq_json) monkeypatch.setattr(ap_gateway, "apGet", mock_ap_get) actor_data = await ap_gateway.getAPActorDataFromId(TEST_AP_ACCOUNT) outbox = await ap_gateway.apGetObject(actor_data, "outbox") items, rsm_resp = await ap_gateway.getAPItems(outbox, 2) assert rsm_resp.count == 4 assert rsm_resp.index == 0 assert rsm_resp.first == "https://example.org/users/test_user/statuses/4" assert rsm_resp.last == "https://example.org/users/test_user/statuses/3" assert items[0].entry.title.toXml() == ( "<title xmlns='http://www.w3.org/2005/Atom' type='xhtml'>" "<div xmlns='http://www.w3.org/1999/xhtml'><p>test message 4</p></div>" "</title>" ) author_uri = str( [e for e in items[0].entry.author.elements() if e.name == "uri"][0] ) assert author_uri == "xmpp:test_user\\40example.org@ap.test.example" assert str(items[0].entry.published) == "2021-12-16T17:25:03Z" assert items[1].entry.title.toXml() == ( "<title xmlns='http://www.w3.org/2005/Atom' type='xhtml'>" "<div xmlns='http://www.w3.org/1999/xhtml'><p>test message 3</p></div>" "</title>" ) author_uri = str( [e for e in items[1].entry.author.elements() if e.name == "uri"][0] ) assert author_uri == "xmpp:test_user\\40example.org@ap.test.example" assert str(items[1].entry.published) == "2021-12-16T17:26:03Z" items, rsm_resp = await ap_gateway.getAPItems( outbox, max_items=2, after_id="https://example.org/users/test_user/statuses/3", ) assert rsm_resp.count == 4 assert rsm_resp.index == 2 assert rsm_resp.first == "https://example.org/users/test_user/statuses/2" assert rsm_resp.last == "https://example.org/users/test_user/statuses/1" assert items[0].entry.title.toXml() == ( "<title xmlns='http://www.w3.org/2005/Atom' type='xhtml'>" "<div xmlns='http://www.w3.org/1999/xhtml'><p>test message 2</p></div>" "</title>" ) author_uri = str( [e for e in items[0].entry.author.elements() if e.name == "uri"][0] ) assert author_uri == "xmpp:test_user\\40example.org@ap.test.example" assert str(items[0].entry.published) == "2021-12-16T17:27:03Z" assert items[1].entry.title.toXml() == ( "<title xmlns='http://www.w3.org/2005/Atom' type='xhtml'>" "<div xmlns='http://www.w3.org/1999/xhtml'><p>test message 1</p></div>" "</title>" ) author_uri = str( [e for e in items[1].entry.author.elements() if e.name == "uri"][0] ) assert author_uri == "xmpp:test_user\\40example.org@ap.test.example" assert str(items[1].entry.published) == "2021-12-16T17:28:03Z" items, rsm_resp = await ap_gateway.getAPItems( outbox, max_items=1, start_index=2 ) assert rsm_resp.count == 4 assert rsm_resp.index == 2 assert rsm_resp.first == "https://example.org/users/test_user/statuses/2" assert rsm_resp.last == "https://example.org/users/test_user/statuses/2" assert len(items) == 1 assert items[0].entry.title.toXml() == ( "<title xmlns='http://www.w3.org/2005/Atom' type='xhtml'>" "<div xmlns='http://www.w3.org/1999/xhtml'><p>test message 2</p></div>" "</title>" ) assert str(items[0].entry.published) == "2021-12-16T17:27:03Z" items, rsm_resp = await ap_gateway.getAPItems( outbox, max_items=3, chronological_pagination=False ) assert rsm_resp.count == 4 assert rsm_resp.index == 1 assert rsm_resp.first == "https://example.org/users/test_user/statuses/3" assert rsm_resp.last == "https://example.org/users/test_user/statuses/1" assert len(items) == 3 assert items[0].entry.title.toXml() == ( "<title xmlns='http://www.w3.org/2005/Atom' type='xhtml'>" "<div xmlns='http://www.w3.org/1999/xhtml'><p>test message 3</p></div>" "</title>" ) assert items[2].entry.title.toXml() == ( "<title xmlns='http://www.w3.org/2005/Atom' type='xhtml'>" "<div xmlns='http://www.w3.org/1999/xhtml'><p>test message 1</p></div>" "</title>" ) def ap_request_params(self, ap_gateway, type_=None, url=None, query_data=None): """Generate parameters for HTTPAPGServer's AP*Request @param type_: one of the AP query type (e.g. "outbox") @param url: URL to query (mutually exclusif with type_) @param query_data: query data as returned by parse.parse_qs @return dict with kw params to use """ assert type_ and url is None or url and type_ is None if type_ is not None: path = f"_ap/{type_}/some_user%40test.example" else: url_parsed = parse.urlparse(url) path = url_parsed.path.lstrip("/") type_ = path.split("/")[1] if query_data is None: query_data = parse.parse_qs(url_parsed.query) if query_data: uri = f"{path}?{parse.urlencode(query_data, doseq=True)}" else: uri = path test_jid = jid.JID("some_user@test.example") request = Request(MagicMock()) request.path = path.encode() request.uri = uri.encode() ap_url = parse.urljoin( f"https://{ap_gateway.public_url}", path ) kwargs = { "request": request, "account_jid": test_jid, "node": None, "ap_account": test_jid.full(), "ap_url": ap_url, } if type_ == "outbox" and query_data: kwargs["query_data"] = query_data return kwargs @ed async def test_pubsub_to_ap_conversion(self, ap_gateway, monkeypatch): """Pubsub nodes are converted to AP collections""" monkeypatch.setattr(ap_gateway._p, "getItems", mock_getItems) outbox = await ap_gateway.server.resource.APOutboxRequest( **self.ap_request_params(ap_gateway, "outbox") ) assert outbox["@context"] == "https://www.w3.org/ns/activitystreams" assert outbox["id"] == "https://test.example/_ap/outbox/some_user%40test.example" assert outbox["totalItems"] == len(XMPP_ITEMS) assert outbox["type"] == "OrderedCollection" assert outbox["first"] assert outbox["last"] first_page = await ap_gateway.server.resource.APOutboxPageRequest( **self.ap_request_params(ap_gateway, url=outbox["first"]) ) assert first_page["@context"] == "https://www.w3.org/ns/activitystreams" assert first_page["id"] == "https://test.example/_ap/outbox/some_user%40test.example?page=first" assert first_page["type"] == "OrderedCollectionPage" assert first_page["partOf"] == outbox["id"] assert len(first_page["orderedItems"]) == len(XMPP_ITEMS) first_item = first_page["orderedItems"][0] assert first_item["@context"] == "https://www.w3.org/ns/activitystreams" assert first_item["id"] == "https://test.example/_ap/item/some_user%40test.example/4" assert first_item["actor"] == "https://test.example/_ap/actor/some_user%40test.example" assert first_item["type"] == "Create" first_item_obj = first_item["object"] assert first_item_obj["id"] == first_item["id"] assert first_item_obj["type"] == "Note" assert first_item_obj["published"] == "2022-01-28T16:02:19Z" assert first_item_obj["attributedTo"] == first_item["actor"] assert first_item_obj["content"] == "<div><p>XMPP item 4</p></div>" assert first_item_obj["to"] == "https://www.w3.org/ns/activitystreams#Public"