comparison 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
comparison
equal deleted inserted replaced
3808:39fc2e1b3793 3809:04b57c0b2278
15 15
16 # You should have received a copy of the GNU Affero General Public License 16 # You should have received a copy of the GNU Affero General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. 17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 18
19 from copy import deepcopy 19 from copy import deepcopy
20 from unittest.mock import MagicMock, AsyncMock, patch 20 from unittest.mock import MagicMock, AsyncMock, patch, DEFAULT
21 from urllib import parse 21 from urllib import parse
22 from functools import partial 22 from functools import partial
23 23
24 import pytest 24 import pytest
25 from pytest_twisted import ensureDeferred as ed 25 from pytest_twisted import ensureDeferred as ed
26 from twisted.internet import defer 26 from twisted.internet import defer
27 from twisted.words.protocols.jabber import jid 27 from twisted.words.protocols.jabber import jid
28 from twisted.web.server import Request 28 from twisted.web.server import Request
29 from twisted.words.xish import domish 29 from twisted.words.xish import domish
30 from wokkel import rsm, pubsub 30 from wokkel import rsm, pubsub
31 from treq.response import _Response as TReqResponse
31 32
32 from sat.core import exceptions 33 from sat.core import exceptions
33 from sat.core.constants import Const as C 34 from sat.core.constants import Const as C
34 from sat.plugins import plugin_comp_ap_gateway 35 from sat.plugins import plugin_comp_ap_gateway
35 from sat.plugins.plugin_comp_ap_gateway import constants as ap_const 36 from sat.plugins.plugin_comp_ap_gateway import constants as ap_const
36 from sat.plugins.plugin_comp_ap_gateway.http_server import HTTPServer 37 from sat.plugins.plugin_comp_ap_gateway.http_server import HTTPServer
37 from sat.plugins.plugin_xep_0277 import NS_ATOM 38 from sat.plugins.plugin_xep_0277 import NS_ATOM
39 from sat.plugins.plugin_xep_0422 import NS_FASTEN
40 from sat.plugins.plugin_xep_0424 import NS_MESSAGE_RETRACT
38 from sat.plugins.plugin_xep_0465 import NS_PPS 41 from sat.plugins.plugin_xep_0465 import NS_PPS
39 from sat.tools.utils import xmpp_date 42 from sat.tools.utils import xmpp_date
40 from sat.tools import xml_tools 43 from sat.tools import xml_tools
41 from sat.plugins.plugin_comp_ap_gateway import TYPE_ACTOR 44 from sat.plugins.plugin_comp_ap_gateway import TYPE_ACTOR
45 from sat.memory.sqla_mapping import SubscriptionState
42 46
43 47
44 TEST_BASE_URL = "https://example.org" 48 TEST_BASE_URL = "https://example.org"
45 TEST_USER = "test_user" 49 TEST_USER = "test_user"
46 TEST_AP_ACCOUNT = f"{TEST_USER}@example.org" 50 TEST_AP_ACCOUNT = f"{TEST_USER}@example.org"
378 client = MagicMock() 382 client = MagicMock()
379 client.jid = jid 383 client.jid = jid
380 return client 384 return client
381 385
382 386
387 class FakeTReqPostResponse:
388 code = 202
389
390
383 @pytest.fixture(scope="session") 391 @pytest.fixture(scope="session")
384 def ap_gateway(host): 392 def ap_gateway(host):
385 gateway = plugin_comp_ap_gateway.APGateway(host) 393 gateway = plugin_comp_ap_gateway.APGateway(host)
386 gateway.initialised = True 394 gateway.initialised = True
387 gateway.isPubsub = AsyncMock() 395 gateway.isPubsub = AsyncMock()
884 # sendMessage must be called for <message> stanza, and the "message" argument must 892 # sendMessage must be called for <message> stanza, and the "message" argument must
885 # be set to the content of the original AP message 893 # be set to the content of the original AP message
886 assert sendMessage.called 894 assert sendMessage.called
887 assert sendMessage.call_args.args[0] == TEST_JID 895 assert sendMessage.call_args.args[0] == TEST_JID
888 assert sendMessage.call_args.args[1] == {"": "test direct message"} 896 assert sendMessage.call_args.args[1] == {"": "test direct message"}
897
898 @ed
899 async def test_pubsub_retract_to_ap_delete(self, ap_gateway, monkeypatch):
900 """Pubsub retract requests are converted to AP delete activity"""
901 monkeypatch.setattr(plugin_comp_ap_gateway.treq, "get", mock_ap_get)
902 monkeypatch.setattr(plugin_comp_ap_gateway.treq, "json_content", mock_treq_json)
903 monkeypatch.setattr(ap_gateway, "apGet", mock_ap_get)
904 retract_id = "retract_123"
905 retract_elt = domish.Element((pubsub.NS_PUBSUB_EVENT, "retract"))
906 retract_elt["id"] = retract_id
907 items_event = pubsub.ItemsEvent(
908 sender=TEST_JID,
909 recipient=ap_gateway.getLocalJIDFromAccount(TEST_AP_ACCOUNT),
910 nodeIdentifier=ap_gateway._m.namespace,
911 items=[retract_elt],
912 headers={}
913 )
914 with patch.object(ap_gateway, "signAndPost") as signAndPost:
915 signAndPost.return_value = FakeTReqPostResponse()
916 # we simulate the reception of a retract event
917 await ap_gateway._itemsReceived(ap_gateway.client, items_event)
918 url, actor_id, doc = signAndPost.call_args[0]
919 jid_account = await ap_gateway.getAPAccountFromJidAndNode(TEST_JID, None)
920 jid_actor_id = ap_gateway.buildAPURL(ap_const.TYPE_ACTOR, jid_account)
921 assert url == f"{TEST_BASE_URL}/users/{TEST_USER}/inbox"
922 assert actor_id == jid_actor_id
923 assert doc["type"] == "Delete"
924 assert doc["actor"] == jid_actor_id
925 obj = doc["object"]
926 assert obj["type"] == ap_const.TYPE_TOMBSTONE
927 url_item_id = ap_gateway.buildAPURL(ap_const.TYPE_ITEM, jid_account, retract_id)
928 assert obj["id"] == url_item_id
929
930 @ed
931 async def test_ap_delete_to_pubsub_retract(self, ap_gateway):
932 """AP delete activity is converted to pubsub retract"""
933 client = ap_gateway.client.getVirtualClient(
934 ap_gateway.getLocalJIDFromAccount(TEST_AP_ACCOUNT)
935 )
936
937 ap_item = {
938 "@context": "https://www.w3.org/ns/activitystreams",
939 "actor": TEST_AP_ACTOR_ID,
940 "id": "https://test.example/retract_123",
941 "type": "Delete",
942 "object": {"id": f"{TEST_AP_ACTOR_ID}/item/123",
943 "type": "Tombstone"},
944 "to": ["https://www.w3.org/ns/activitystreams#Public"]
945 }
946 with patch.multiple(
947 ap_gateway.host.memory.storage,
948 get=DEFAULT,
949 getPubsubNode=DEFAULT,
950 deletePubsubItems=DEFAULT,
951 ) as mock_objs:
952 mock_objs["get"].return_value=None
953 cached_node = MagicMock()
954 mock_objs["getPubsubNode"].return_value=cached_node
955 subscription = MagicMock()
956 subscription.state = SubscriptionState.SUBSCRIBED
957 subscription.subscriber = TEST_JID
958 cached_node.subscriptions = [subscription]
959 with patch.object(
960 ap_gateway.pubsub_service, "notifyRetract"
961 ) as notifyRetract:
962 # we simulate a received Delete activity
963 await ap_gateway.newAPDeleteItem(
964 client=client,
965 destinee=None,
966 node=ap_gateway._m.namespace,
967 item=ap_item
968 )
969
970 # item is deleted from database
971 deletePubsubItems = mock_objs["deletePubsubItems"]
972 assert deletePubsubItems.call_count == 1
973 assert deletePubsubItems.call_args.args[1] == [ap_item["id"]]
974
975 # retraction notification is sent to subscribers
976 assert notifyRetract.call_count == 1
977 assert notifyRetract.call_args.args[0] == client.jid
978 assert notifyRetract.call_args.args[1] == ap_gateway._m.namespace
979 notifications = notifyRetract.call_args.args[2]
980 assert len(notifications) == 1
981 subscriber, __, item_elts = notifications[0]
982 assert subscriber == TEST_JID
983 assert len(item_elts) == 1
984 item_elt = item_elts[0]
985 assert isinstance(item_elt, domish.Element)
986 assert item_elt.name == "item"
987 assert item_elt["id"] == ap_item["id"]
988
989 @ed
990 async def test_message_retract_to_ap_delete(self, ap_gateway, monkeypatch):
991 """Message retract requests are converted to AP delete activity"""
992 monkeypatch.setattr(plugin_comp_ap_gateway.treq, "get", mock_ap_get)
993 monkeypatch.setattr(plugin_comp_ap_gateway.treq, "json_content", mock_treq_json)
994 monkeypatch.setattr(ap_gateway, "apGet", mock_ap_get)
995 # origin ID is the ID of the message to retract
996 origin_id = "mess_retract_123"
997
998 # we call retractByOriginId to get the message element of a retraction request
999 fake_client = MagicMock()
1000 fake_client.jid = TEST_JID
1001 dest_jid = ap_gateway.getLocalJIDFromAccount(TEST_AP_ACCOUNT)
1002 ap_gateway._r.retractByOriginId(fake_client, dest_jid, origin_id)
1003 # message_retract_elt is the message which would be sent for a retraction
1004 message_retract_elt = fake_client.send.call_args.args[0]
1005 apply_to_elt = next(message_retract_elt.elements(NS_FASTEN, "apply-to"))
1006 retract_elt = apply_to_elt.retract
1007
1008 with patch.object(ap_gateway, "signAndPost") as signAndPost:
1009 signAndPost.return_value = FakeTReqPostResponse()
1010 fake_fastened_elts = MagicMock()
1011 fake_fastened_elts.id = origin_id
1012 # we simulate the reception of a retract event using the message element that
1013 # we generated above
1014 await ap_gateway._onMessageRetract(
1015 ap_gateway.client,
1016 message_retract_elt,
1017 retract_elt,
1018 fake_fastened_elts
1019 )
1020 url, actor_id, doc = signAndPost.call_args[0]
1021
1022 # the AP delete activity must have been sent through signAndPost
1023 # we check its values
1024 jid_account = await ap_gateway.getAPAccountFromJidAndNode(TEST_JID, None)
1025 jid_actor_id = ap_gateway.buildAPURL(ap_const.TYPE_ACTOR, jid_account)
1026 assert url == f"{TEST_BASE_URL}/users/{TEST_USER}/inbox"
1027 assert actor_id == jid_actor_id
1028 assert doc["type"] == "Delete"
1029 assert doc["actor"] == jid_actor_id
1030 obj = doc["object"]
1031 assert obj["type"] == ap_const.TYPE_TOMBSTONE
1032 url_item_id = ap_gateway.buildAPURL(ap_const.TYPE_ITEM, jid_account, origin_id)
1033 assert obj["id"] == url_item_id
1034
1035 @ed
1036 async def test_ap_delete_to_message_retract(self, ap_gateway, monkeypatch):
1037 """AP delete activity is converted to message retract"""
1038 # note: a message retract is used when suitable message is found in history,
1039 # otherwise it should be in pubsub cache and it's a pubsub retract (tested above
1040 # by ``test_ap_delete_to_pubsub_retract``)
1041
1042 # we don't want actual queries in database
1043 retractDBHistory = AsyncMock()
1044 monkeypatch.setattr(ap_gateway._r, "retractDBHistory", retractDBHistory)
1045
1046 client = ap_gateway.client.getVirtualClient(
1047 ap_gateway.getLocalJIDFromAccount(TEST_AP_ACCOUNT)
1048 )
1049 fake_send = MagicMock()
1050 monkeypatch.setattr(client, "send", fake_send)
1051
1052 ap_item = {
1053 "@context": "https://www.w3.org/ns/activitystreams",
1054 "actor": TEST_AP_ACTOR_ID,
1055 "id": "https://test.example/retract_123",
1056 "type": "Delete",
1057 "object": {"id": f"{TEST_AP_ACTOR_ID}/item/123",
1058 "type": "Tombstone"},
1059 "to": ["https://www.w3.org/ns/activitystreams#Public"]
1060 }
1061 with patch.object(ap_gateway.host.memory.storage, "get") as storage_get:
1062 fake_history = MagicMock()
1063 fake_history.source_jid = client.jid
1064 fake_history.dest_jid = TEST_JID
1065 fake_history.origin_id = ap_item["id"]
1066 storage_get.return_value = fake_history
1067 # we simulate a received Delete activity
1068 await ap_gateway.newAPDeleteItem(
1069 client=client,
1070 destinee=None,
1071 node=ap_gateway._m.namespace,
1072 item=ap_item
1073 )
1074
1075 # item is deleted from database
1076 assert retractDBHistory.call_count == 1
1077 assert retractDBHistory.call_args.args[0] == client
1078 assert retractDBHistory.call_args.args[1] == fake_history
1079
1080 # retraction notification is sent to destinee
1081 assert fake_send.call_count == 1
1082 sent_elt = fake_send.call_args.args[0]
1083 assert sent_elt.name == "message"
1084 assert sent_elt["from"] == client.jid.full()
1085 assert sent_elt["to"] == TEST_JID.full()
1086 apply_to_elt = next(sent_elt.elements(NS_FASTEN, "apply-to"))
1087 assert apply_to_elt["id"] == ap_item["id"]
1088 retract_elt = apply_to_elt.retract
1089 assert retract_elt is not None
1090 assert retract_elt.uri == NS_MESSAGE_RETRACT