# HG changeset patch # User Goffi # Date 1655473834 -7200 # Node ID 04b57c0b2278a78f330339483acee37f449e23f7 # Parent 39fc2e1b37939517fe4d53c8f44f7dacf3993218 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 diff -r 39fc2e1b3793 -r 04b57c0b2278 tests/unit/test_ap-gateway.py --- a/tests/unit/test_ap-gateway.py Fri Jun 17 15:50:34 2022 +0200 +++ b/tests/unit/test_ap-gateway.py Fri Jun 17 15:50:34 2022 +0200 @@ -17,7 +17,7 @@ # along with this program. If not, see . from copy import deepcopy -from unittest.mock import MagicMock, AsyncMock, patch +from unittest.mock import MagicMock, AsyncMock, patch, DEFAULT from urllib import parse from functools import partial @@ -28,6 +28,7 @@ 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 @@ -35,10 +36,13 @@ 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" @@ -380,6 +384,10 @@ return client +class FakeTReqPostResponse: + code = 202 + + @pytest.fixture(scope="session") def ap_gateway(host): gateway = plugin_comp_ap_gateway.APGateway(host) @@ -886,3 +894,197 @@ 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