diff tests/unit/test_ap-gateway.py @ 3809:04b57c0b2278

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
author Goffi <goffi@goffi.org>
date Fri, 17 Jun 2022 15:50:34 +0200
parents 39fc2e1b3793
children 81c79b7cafa7
line wrap: on
line diff
--- 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 <http://www.gnu.org/licenses/>.
 
 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