libervia-backend/tests/unit/test_ap-gateway.py
Goffi 57c945dc9f tests (unit/ap gateway): message/item retractation tests:
this patch adds 4 tests to check pubsub <=> AP item retractation and message <=> AP item
retractation.

rel 367
2022-06-17 15:50:34 +02:00

1090 lines
44 KiB
Python

#!/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, AsyncMock, patch, DEFAULT
from urllib import parse
from functools import partial
import pytest
from pytest_twisted import ensureDeferred as ed
from twisted.internet import defer
from twisted.words.protocols.jabber import jid
from twisted.web.server import Request
from twisted.words.xish import domish
from wokkel import rsm, pubsub
from treq.response import _Response as TReqResponse
from sat.core import exceptions
from sat.core.constants import Const as C
from sat.plugins import plugin_comp_ap_gateway
from sat.plugins.plugin_comp_ap_gateway import constants as ap_const
from sat.plugins.plugin_comp_ap_gateway.http_server import HTTPServer
from sat.plugins.plugin_xep_0277 import NS_ATOM
from sat.plugins.plugin_xep_0422 import NS_FASTEN
from sat.plugins.plugin_xep_0424 import NS_MESSAGE_RETRACT
from sat.plugins.plugin_xep_0465 import NS_PPS
from sat.tools.utils import xmpp_date
from sat.tools import xml_tools
from sat.plugins.plugin_comp_ap_gateway import TYPE_ACTOR
from sat.memory.sqla_mapping import SubscriptionState
TEST_BASE_URL = "https://example.org"
TEST_USER = "test_user"
TEST_AP_ACCOUNT = f"{TEST_USER}@example.org"
TEST_AP_ACTOR_ID = f"{TEST_BASE_URL}/users/{TEST_USER}"
PUBLIC_URL = "test.example"
TEST_JID = jid.JID(f"some_user@{PUBLIC_URL}")
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}/.well-known/webfinger?"
f"resource=acct:{parse.quote('ext_user@example.org')}": {
"aliases": [
f"{TEST_BASE_URL}/@ext_user",
f"{TEST_BASE_URL}/users/ext_user"
],
"links": [
{
"href": f"{TEST_BASE_URL}/users/ext_user",
"rel": "self",
"type": "application/activity+json"
},
],
"subject": f"acct:ext_user@example.org"
},
f"{TEST_BASE_URL}/users/ext_user": {
"@context": [
"https://www.w3.org/ns/activitystreams",
],
"endpoints": {
"sharedInbox": f"{TEST_BASE_URL}/inbox"
},
"followers": f"{TEST_BASE_URL}/users/ext_user/followers",
"following": f"{TEST_BASE_URL}/users/ext_user/following",
"id": f"{TEST_BASE_URL}/users/ext_user",
"inbox": f"{TEST_BASE_URL}/users/ext_user/inbox",
"name": "",
"outbox": f"{TEST_BASE_URL}/users/ext_user/outbox",
"preferredUsername": f"ext_user",
"type": "Person",
"url": f"{TEST_BASE_URL}/@ext_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"
},
f"{TEST_BASE_URL}/users/{TEST_USER}/following": {
"@context": "https://www.w3.org/ns/activitystreams",
"first": f"{TEST_BASE_URL}/users/{TEST_USER}/following?page=1",
"id": f"{TEST_BASE_URL}/users/{TEST_USER}/following",
"totalItems": 2,
"type": "OrderedCollection"
},
f"{TEST_BASE_URL}/users/{TEST_USER}/following?page=1": {
"@context": "https://www.w3.org/ns/activitystreams",
"id": f"{TEST_BASE_URL}/users/{TEST_USER}/following?page=1",
"orderedItems": [
f"{TEST_BASE_URL}/users/ext_user",
f"https://{PUBLIC_URL}/_ap/{TYPE_ACTOR}/local_user%40{PUBLIC_URL}",
],
"partOf": "{TEST_BASE_URL}/users/{TEST_USER}/following",
"totalItems": 2,
"type": "OrderedCollectionPage"
},
f"{TEST_BASE_URL}/users/{TEST_USER}/followers": {
"@context": "https://www.w3.org/ns/activitystreams",
"first": f"{TEST_BASE_URL}/users/{TEST_USER}/followers?page=1",
"id": f"{TEST_BASE_URL}/users/{TEST_USER}/followers",
"totalItems": 2,
"type": "OrderedCollection"
},
f"{TEST_BASE_URL}/users/{TEST_USER}/followers?page=1": {
"@context": "https://www.w3.org/ns/activitystreams",
"id": f"{TEST_BASE_URL}/users/{TEST_USER}/followers?page=1",
"orderedItems": [
f"{TEST_BASE_URL}/users/ext_user",
f"https://{PUBLIC_URL}/_ap/{TYPE_ACTOR}/local_user%40{PUBLIC_URL}",
],
"partOf": "{TEST_BASE_URL}/users/{TEST_USER}/followers",
"totalItems": 2,
"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):
ret_items = kwargs.pop("ret_items", XMPP_ITEMS)
rsm_resp = rsm.RSMResponse(
first=ret_items[0]["id"],
last=ret_items[-1]["id"],
index=0,
count=len(ret_items)
)
return ret_items, {"rsm": rsm_resp.toDict(), "complete": True}
def getVirtualClient(jid):
client = MagicMock()
client.jid = jid
return client
class FakeTReqPostResponse:
code = 202
@pytest.fixture(scope="session")
def ap_gateway(host):
gateway = plugin_comp_ap_gateway.APGateway(host)
gateway.initialised = True
gateway.isPubsub = AsyncMock()
gateway.isPubsub.return_value = False
client = MagicMock()
client.jid = jid.JID("ap.test.example")
client.host = "test.example"
client.getVirtualClient = getVirtualClient
gateway.client = client
gateway.local_only = True
gateway.public_url = PUBLIC_URL
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:
def getTitleXHTML(self, item_elt: domish.Element) -> domish.Element:
return next(
t for t in item_elt.entry.elements(NS_ATOM, "title")
if t.getAttribute("type") == "xhtml"
)
@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.getAPActorDataFromAccount(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"
title_xhtml = self.getTitleXHTML(items[0])
assert title_xhtml.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"
title_xhtml = self.getTitleXHTML(items[1])
assert title_xhtml.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"
title_xhtml = self.getTitleXHTML(items[0])
assert title_xhtml.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"
title_xhtml = self.getTitleXHTML(items[1])
assert title_xhtml.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
title_xhtml = self.getTitleXHTML(items[0])
assert title_xhtml.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
title_xhtml = self.getTitleXHTML(items[0])
assert title_xhtml.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>"
)
title_xhtml = self.getTitleXHTML(items[2])
assert title_xhtml.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,
"signing_actor": None
}
if type_ == "outbox" and query_data:
kwargs["query_data"] = query_data
# signing_actor is not used for page requests
del kwargs["signing_actor"]
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"]
@ed
async def test_following_to_pps(self, ap_gateway, monkeypatch):
"""AP following items are converted to Public Pubsub Subscription subscriptions"""
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)
items, __ = await ap_gateway.pubsub_service.items(
jid.JID("toto@example.org"),
ap_gateway.getLocalJIDFromAccount(TEST_AP_ACCOUNT),
ap_gateway._pps.subscriptions_node,
None,
None,
None
)
assert len(items) == 2
for idx, entity in enumerate((
"local_user@test.example",
"ext_user\\40example.org@ap.test.example"
)):
subscription_elt = next(items[idx].elements(NS_PPS, "subscription"), None)
assert subscription_elt is not None
assert subscription_elt["node"] == ap_gateway._m.namespace
assert subscription_elt["service"] == entity
@ed
async def test_followers_to_pps(self, ap_gateway, monkeypatch):
"""AP followers items are converted to Public Pubsub Subscription subscribers"""
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)
items, __ = await ap_gateway.pubsub_service.items(
jid.JID("toto@example.org"),
ap_gateway.getLocalJIDFromAccount(TEST_AP_ACCOUNT),
ap_gateway._pps.getPublicSubscribersNode(ap_gateway._m.namespace),
None,
None,
None
)
assert len(items) == 2
for idx, entity in enumerate((
"local_user@test.example",
"ext_user\\40example.org@ap.test.example"
)):
subscriber_elt = next(items[idx].elements(NS_PPS, "subscriber"), None)
assert subscriber_elt is not None
assert subscriber_elt["jid"] == entity
@ed
async def test_pps_to_following(self, ap_gateway, monkeypatch):
"""Public Pubsub Subscription subscriptions are converted to AP following items"""
subscriptions = [
pubsub.Item(
id="subscription_1",
payload = ap_gateway._pps.buildSubscriptionElt(
ap_gateway._m.namespace,
jid.JID("local_user@test.example")
)
),
pubsub.Item(
id="subscription_2",
payload = ap_gateway._pps.buildSubscriptionElt(
ap_gateway._m.namespace,
jid.JID("ext_user\\40example.org@ap.test.example")
)
)
]
monkeypatch.setattr(ap_gateway._p, "getItems", partial(
mock_getItems,
ret_items=subscriptions
))
following = await ap_gateway.server.resource.APFollowingRequest(
**self.ap_request_params(ap_gateway, "following")
)
assert following["@context"] == "https://www.w3.org/ns/activitystreams"
assert following["id"] == "https://test.example/_ap/following/some_user%40test.example"
assert following["totalItems"] == len(subscriptions)
assert following["type"] == "OrderedCollection"
assert following.get("first")
first_page = following["first"]
assert first_page["type"] == "OrderedCollectionPage"
assert len(first_page["orderedItems"]) == len(subscriptions)
items = first_page["orderedItems"]
assert items == ['local_user@test.example', 'ext_user@example.org']
@ed
async def test_pps_to_followers(self, ap_gateway, monkeypatch):
"""Public Pubsub Subscription subscribers are converted to AP followers"""
subscribers = [
pubsub.Item(
id="subscriber_1",
payload = ap_gateway._pps.buildSubscriberElt(
jid.JID("local_user@test.example")
)
),
pubsub.Item(
id="subscriber_2",
payload = ap_gateway._pps.buildSubscriberElt(
jid.JID("ext_user\\40example.org@ap.test.example")
)
)
]
monkeypatch.setattr(ap_gateway._p, "getItems", partial(
mock_getItems,
ret_items=subscribers
))
followers = await ap_gateway.server.resource.APFollowersRequest(
**self.ap_request_params(ap_gateway, "followers")
)
assert followers["@context"] == "https://www.w3.org/ns/activitystreams"
assert followers["id"] == "https://test.example/_ap/followers/some_user%40test.example"
assert followers["totalItems"] == len(subscribers)
assert followers["type"] == "OrderedCollection"
assert followers.get("first")
first_page = followers["first"]
assert first_page["type"] == "OrderedCollectionPage"
assert len(first_page["orderedItems"]) == len(subscribers)
items = first_page["orderedItems"]
assert items == ['local_user@test.example', 'ext_user@example.org']
@ed
async def test_xmpp_message_to_ap_direct_message(self, ap_gateway, monkeypatch):
"""XMPP message are sent as AP direct message"""
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)
mess_data = {
"from": TEST_JID,
"to": ap_gateway.getLocalJIDFromAccount(TEST_AP_ACCOUNT),
"type": "chat",
"message": {"": "This is a test message."},
"extra": {
"origin-id": "123"
}
}
with patch.object(ap_gateway, "signAndPost") as signAndPost:
await ap_gateway.onMessage(ap_gateway.client, mess_data)
url, actor_id, doc = signAndPost.call_args[0]
assert url == "https://example.org/users/test_user/inbox"
assert actor_id == "https://test.example/_ap/actor/some_user%40test.example"
obj = doc["object"]
assert doc["@context"] == "https://www.w3.org/ns/activitystreams"
assert doc["actor"] == "https://test.example/_ap/actor/some_user%40test.example"
assert obj["type"] == "Note"
assert obj["content"] == "This is a test message."
assert obj["attributedTo"] == (
"https://test.example/_ap/actor/some_user%40test.example"
)
# we must have a direct message, thus the item must be only addressed to destinee
# ("to" attribute of the message), and the "Public" namespace must not be set
assert doc["to"] == ["https://example.org/users/test_user"]
assert obj["to"] == ["https://example.org/users/test_user"]
for field in ("bto", "cc", "bcc", "audience"):
assert field not in doc
assert field not in obj
@ed
async def test_ap_direct_message_to_xmpp_message(self, ap_gateway, monkeypatch):
"""AP direct message are sent as XMPP message (not 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)
# we have to patch DeferredList to not wait forever
monkeypatch.setattr(defer, "DeferredList", AsyncMock())
xmpp_actor_id = ap_gateway.buildAPURL(ap_const.TYPE_ACTOR, TEST_JID.userhost())
direct_ap_message = {
'attributedTo': TEST_AP_ACTOR_ID,
'cc': [],
'content': '<p>test direct message</p>',
'contentMap': {'en': '<p>test direct message</p>'},
'id': f'{TEST_AP_ACTOR_ID}/statuses/123',
'published': '2022-05-20T08:14:39Z',
'to': [xmpp_actor_id],
'type': 'Note',
}
client = ap_gateway.client.getVirtualClient(
ap_gateway.getLocalJIDFromAccount(TEST_AP_ACCOUNT)
)
with patch.object(client, "sendMessage") as sendMessage:
await ap_gateway.newAPItem(
client, None, ap_gateway._m.namespace, direct_ap_message
)
# sendMessage must be called for <message> stanza, and the "message" argument must
# be set to the content of the original AP message
assert sendMessage.called
assert sendMessage.call_args.args[0] == TEST_JID
assert sendMessage.call_args.args[1] == {"": "test direct message"}
@ed
async def test_pubsub_retract_to_ap_delete(self, ap_gateway, monkeypatch):
"""Pubsub retract requests are converted to AP delete activity"""
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)
retract_id = "retract_123"
retract_elt = domish.Element((pubsub.NS_PUBSUB_EVENT, "retract"))
retract_elt["id"] = retract_id
items_event = pubsub.ItemsEvent(
sender=TEST_JID,
recipient=ap_gateway.getLocalJIDFromAccount(TEST_AP_ACCOUNT),
nodeIdentifier=ap_gateway._m.namespace,
items=[retract_elt],
headers={}
)
with patch.object(ap_gateway, "signAndPost") as signAndPost:
signAndPost.return_value = FakeTReqPostResponse()
# we simulate the reception of a retract event
await ap_gateway._itemsReceived(ap_gateway.client, items_event)
url, actor_id, doc = signAndPost.call_args[0]
jid_account = await ap_gateway.getAPAccountFromJidAndNode(TEST_JID, None)
jid_actor_id = ap_gateway.buildAPURL(ap_const.TYPE_ACTOR, jid_account)
assert url == f"{TEST_BASE_URL}/users/{TEST_USER}/inbox"
assert actor_id == jid_actor_id
assert doc["type"] == "Delete"
assert doc["actor"] == jid_actor_id
obj = doc["object"]
assert obj["type"] == ap_const.TYPE_TOMBSTONE
url_item_id = ap_gateway.buildAPURL(ap_const.TYPE_ITEM, jid_account, retract_id)
assert obj["id"] == url_item_id
@ed
async def test_ap_delete_to_pubsub_retract(self, ap_gateway):
"""AP delete activity is converted to pubsub retract"""
client = ap_gateway.client.getVirtualClient(
ap_gateway.getLocalJIDFromAccount(TEST_AP_ACCOUNT)
)
ap_item = {
"@context": "https://www.w3.org/ns/activitystreams",
"actor": TEST_AP_ACTOR_ID,
"id": "https://test.example/retract_123",
"type": "Delete",
"object": {"id": f"{TEST_AP_ACTOR_ID}/item/123",
"type": "Tombstone"},
"to": ["https://www.w3.org/ns/activitystreams#Public"]
}
with patch.multiple(
ap_gateway.host.memory.storage,
get=DEFAULT,
getPubsubNode=DEFAULT,
deletePubsubItems=DEFAULT,
) as mock_objs:
mock_objs["get"].return_value=None
cached_node = MagicMock()
mock_objs["getPubsubNode"].return_value=cached_node
subscription = MagicMock()
subscription.state = SubscriptionState.SUBSCRIBED
subscription.subscriber = TEST_JID
cached_node.subscriptions = [subscription]
with patch.object(
ap_gateway.pubsub_service, "notifyRetract"
) as notifyRetract:
# we simulate a received Delete activity
await ap_gateway.newAPDeleteItem(
client=client,
destinee=None,
node=ap_gateway._m.namespace,
item=ap_item
)
# item is deleted from database
deletePubsubItems = mock_objs["deletePubsubItems"]
assert deletePubsubItems.call_count == 1
assert deletePubsubItems.call_args.args[1] == [ap_item["id"]]
# retraction notification is sent to subscribers
assert notifyRetract.call_count == 1
assert notifyRetract.call_args.args[0] == client.jid
assert notifyRetract.call_args.args[1] == ap_gateway._m.namespace
notifications = notifyRetract.call_args.args[2]
assert len(notifications) == 1
subscriber, __, item_elts = notifications[0]
assert subscriber == TEST_JID
assert len(item_elts) == 1
item_elt = item_elts[0]
assert isinstance(item_elt, domish.Element)
assert item_elt.name == "item"
assert item_elt["id"] == ap_item["id"]
@ed
async def test_message_retract_to_ap_delete(self, ap_gateway, monkeypatch):
"""Message retract requests are converted to AP delete activity"""
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)
# origin ID is the ID of the message to retract
origin_id = "mess_retract_123"
# we call retractByOriginId to get the message element of a retraction request
fake_client = MagicMock()
fake_client.jid = TEST_JID
dest_jid = ap_gateway.getLocalJIDFromAccount(TEST_AP_ACCOUNT)
ap_gateway._r.retractByOriginId(fake_client, dest_jid, origin_id)
# message_retract_elt is the message which would be sent for a retraction
message_retract_elt = fake_client.send.call_args.args[0]
apply_to_elt = next(message_retract_elt.elements(NS_FASTEN, "apply-to"))
retract_elt = apply_to_elt.retract
with patch.object(ap_gateway, "signAndPost") as signAndPost:
signAndPost.return_value = FakeTReqPostResponse()
fake_fastened_elts = MagicMock()
fake_fastened_elts.id = origin_id
# we simulate the reception of a retract event using the message element that
# we generated above
await ap_gateway._onMessageRetract(
ap_gateway.client,
message_retract_elt,
retract_elt,
fake_fastened_elts
)
url, actor_id, doc = signAndPost.call_args[0]
# the AP delete activity must have been sent through signAndPost
# we check its values
jid_account = await ap_gateway.getAPAccountFromJidAndNode(TEST_JID, None)
jid_actor_id = ap_gateway.buildAPURL(ap_const.TYPE_ACTOR, jid_account)
assert url == f"{TEST_BASE_URL}/users/{TEST_USER}/inbox"
assert actor_id == jid_actor_id
assert doc["type"] == "Delete"
assert doc["actor"] == jid_actor_id
obj = doc["object"]
assert obj["type"] == ap_const.TYPE_TOMBSTONE
url_item_id = ap_gateway.buildAPURL(ap_const.TYPE_ITEM, jid_account, origin_id)
assert obj["id"] == url_item_id
@ed
async def test_ap_delete_to_message_retract(self, ap_gateway, monkeypatch):
"""AP delete activity is converted to message retract"""
# note: a message retract is used when suitable message is found in history,
# otherwise it should be in pubsub cache and it's a pubsub retract (tested above
# by ``test_ap_delete_to_pubsub_retract``)
# we don't want actual queries in database
retractDBHistory = AsyncMock()
monkeypatch.setattr(ap_gateway._r, "retractDBHistory", retractDBHistory)
client = ap_gateway.client.getVirtualClient(
ap_gateway.getLocalJIDFromAccount(TEST_AP_ACCOUNT)
)
fake_send = MagicMock()
monkeypatch.setattr(client, "send", fake_send)
ap_item = {
"@context": "https://www.w3.org/ns/activitystreams",
"actor": TEST_AP_ACTOR_ID,
"id": "https://test.example/retract_123",
"type": "Delete",
"object": {"id": f"{TEST_AP_ACTOR_ID}/item/123",
"type": "Tombstone"},
"to": ["https://www.w3.org/ns/activitystreams#Public"]
}
with patch.object(ap_gateway.host.memory.storage, "get") as storage_get:
fake_history = MagicMock()
fake_history.source_jid = client.jid
fake_history.dest_jid = TEST_JID
fake_history.origin_id = ap_item["id"]
storage_get.return_value = fake_history
# we simulate a received Delete activity
await ap_gateway.newAPDeleteItem(
client=client,
destinee=None,
node=ap_gateway._m.namespace,
item=ap_item
)
# item is deleted from database
assert retractDBHistory.call_count == 1
assert retractDBHistory.call_args.args[0] == client
assert retractDBHistory.call_args.args[1] == fake_history
# retraction notification is sent to destinee
assert fake_send.call_count == 1
sent_elt = fake_send.call_args.args[0]
assert sent_elt.name == "message"
assert sent_elt["from"] == client.jid.full()
assert sent_elt["to"] == TEST_JID.full()
apply_to_elt = next(sent_elt.elements(NS_FASTEN, "apply-to"))
assert apply_to_elt["id"] == ap_item["id"]
retract_elt = apply_to_elt.retract
assert retract_elt is not None
assert retract_elt.uri == NS_MESSAGE_RETRACT