comparison tests/unit/test_ap-gateway.py @ 3770:f31113777881

tests (unit/ap-gateway): tests for following/followers <=> PPS: AP following/followers collections to XMPP Public Pubsub Subscriptions conversion is checked in both directions. rel 365
author Goffi <goffi@goffi.org>
date Fri, 13 May 2022 19:29:17 +0200
parents 04ecc8eeb81a
children fedbf7aade11
comparison
equal deleted inserted replaced
3769:a75874df92b8 3770:f31113777881
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, patch 20 from unittest.mock import MagicMock, AsyncMock, patch
21 from urllib import parse 21 from urllib import parse
22 from functools import partial
22 23
23 import pytest 24 import pytest
24 from pytest_twisted import ensureDeferred as ed 25 from pytest_twisted import ensureDeferred as ed
25 from twisted.words.protocols.jabber import jid 26 from twisted.words.protocols.jabber import jid
26 from twisted.web.server import Request 27 from twisted.web.server import Request
27 from wokkel import rsm, pubsub 28 from wokkel import rsm, pubsub
28 29
29 from sat.core import exceptions 30 from sat.core import exceptions
30 from sat.plugins import plugin_comp_ap_gateway 31 from sat.plugins import plugin_comp_ap_gateway
31 from sat.plugins.plugin_comp_ap_gateway.http_server import HTTPServer 32 from sat.plugins.plugin_comp_ap_gateway.http_server import HTTPServer
33 from sat.plugins.plugin_xep_0465 import NS_PPS
32 from sat.tools.utils import xmpp_date 34 from sat.tools.utils import xmpp_date
33 from sat.tools import xml_tools 35 from sat.tools import xml_tools
36 from sat.plugins.plugin_comp_ap_gateway import TYPE_ACTOR
34 37
35 38
36 TEST_BASE_URL = "https://example.org" 39 TEST_BASE_URL = "https://example.org"
37 TEST_USER = "test_user" 40 TEST_USER = "test_user"
38 TEST_AP_ACCOUNT = f"{TEST_USER}@example.org" 41 TEST_AP_ACCOUNT = f"{TEST_USER}@example.org"
42 PUBLIC_URL = "test.example"
39 43
40 AP_REQUESTS = { 44 AP_REQUESTS = {
41 f"{TEST_BASE_URL}/.well-known/webfinger?" 45 f"{TEST_BASE_URL}/.well-known/webfinger?"
42 f"resource=acct:{parse.quote(TEST_AP_ACCOUNT)}": { 46 f"resource=acct:{parse.quote(TEST_AP_ACCOUNT)}": {
43 "aliases": [ 47 "aliases": [
69 "outbox": f"{TEST_BASE_URL}/users/{TEST_USER}/outbox", 73 "outbox": f"{TEST_BASE_URL}/users/{TEST_USER}/outbox",
70 "preferredUsername": f"{TEST_USER}", 74 "preferredUsername": f"{TEST_USER}",
71 "type": "Person", 75 "type": "Person",
72 "url": f"{TEST_BASE_URL}/@{TEST_USER}" 76 "url": f"{TEST_BASE_URL}/@{TEST_USER}"
73 }, 77 },
74 78 f"{TEST_BASE_URL}/.well-known/webfinger?"
79 f"resource=acct:{parse.quote('ext_user@example.org')}": {
80 "aliases": [
81 f"{TEST_BASE_URL}/@ext_user",
82 f"{TEST_BASE_URL}/users/ext_user"
83 ],
84 "links": [
85 {
86 "href": f"{TEST_BASE_URL}/users/ext_user",
87 "rel": "self",
88 "type": "application/activity+json"
89 },
90 ],
91 "subject": f"acct:ext_user@example.org"
92 },
93 f"{TEST_BASE_URL}/users/ext_user": {
94 "@context": [
95 "https://www.w3.org/ns/activitystreams",
96 ],
97 "endpoints": {
98 "sharedInbox": f"{TEST_BASE_URL}/inbox"
99 },
100 "followers": f"{TEST_BASE_URL}/users/ext_user/followers",
101 "following": f"{TEST_BASE_URL}/users/ext_user/following",
102 "id": f"{TEST_BASE_URL}/users/ext_user",
103 "inbox": f"{TEST_BASE_URL}/users/ext_user/inbox",
104 "name": "",
105 "outbox": f"{TEST_BASE_URL}/users/ext_user/outbox",
106 "preferredUsername": f"ext_user",
107 "type": "Person",
108 "url": f"{TEST_BASE_URL}/@ext_user"
109 },
75 f"{TEST_BASE_URL}/users/{TEST_USER}/outbox": { 110 f"{TEST_BASE_URL}/users/{TEST_USER}/outbox": {
76 "@context": "https://www.w3.org/ns/activitystreams", 111 "@context": "https://www.w3.org/ns/activitystreams",
77 "first": f"{TEST_BASE_URL}/users/{TEST_USER}/outbox?page=true", 112 "first": f"{TEST_BASE_URL}/users/{TEST_USER}/outbox?page=true",
78 "id": f"{TEST_BASE_URL}/users/{TEST_USER}/outbox", 113 "id": f"{TEST_BASE_URL}/users/{TEST_USER}/outbox",
79 "last": f"{TEST_BASE_URL}/users/{TEST_USER}/outbox?page=true", 114 "last": f"{TEST_BASE_URL}/users/{TEST_USER}/outbox?page=true",
220 }, 255 },
221 ], 256 ],
222 "partOf": f"{TEST_BASE_URL}/users/{TEST_USER}/outbox", 257 "partOf": f"{TEST_BASE_URL}/users/{TEST_USER}/outbox",
223 "prev": None, 258 "prev": None,
224 "type": "OrderedCollectionPage" 259 "type": "OrderedCollectionPage"
225 } 260 },
261 f"{TEST_BASE_URL}/users/{TEST_USER}/following": {
262 "@context": "https://www.w3.org/ns/activitystreams",
263 "first": f"{TEST_BASE_URL}/users/{TEST_USER}/following?page=1",
264 "id": f"{TEST_BASE_URL}/users/{TEST_USER}/following",
265 "totalItems": 2,
266 "type": "OrderedCollection"
267 },
268 f"{TEST_BASE_URL}/users/{TEST_USER}/following?page=1": {
269 "@context": "https://www.w3.org/ns/activitystreams",
270 "id": f"{TEST_BASE_URL}/users/{TEST_USER}/following?page=1",
271 "orderedItems": [
272 f"{TEST_BASE_URL}/users/ext_user",
273 f"https://{PUBLIC_URL}/_ap/{TYPE_ACTOR}/local_user%40{PUBLIC_URL}",
274 ],
275 "partOf": "{TEST_BASE_URL}/users/{TEST_USER}/following",
276 "totalItems": 2,
277 "type": "OrderedCollectionPage"
278 },
279 f"{TEST_BASE_URL}/users/{TEST_USER}/followers": {
280 "@context": "https://www.w3.org/ns/activitystreams",
281 "first": f"{TEST_BASE_URL}/users/{TEST_USER}/followers?page=1",
282 "id": f"{TEST_BASE_URL}/users/{TEST_USER}/followers",
283 "totalItems": 2,
284 "type": "OrderedCollection"
285 },
286 f"{TEST_BASE_URL}/users/{TEST_USER}/followers?page=1": {
287 "@context": "https://www.w3.org/ns/activitystreams",
288 "id": f"{TEST_BASE_URL}/users/{TEST_USER}/followers?page=1",
289 "orderedItems": [
290 f"{TEST_BASE_URL}/users/ext_user",
291 f"https://{PUBLIC_URL}/_ap/{TYPE_ACTOR}/local_user%40{PUBLIC_URL}",
292 ],
293 "partOf": "{TEST_BASE_URL}/users/{TEST_USER}/followers",
294 "totalItems": 2,
295 "type": "OrderedCollectionPage"
296 },
226 297
227 } 298 }
228 299
229 XMPP_ITEM_TPL = """ 300 XMPP_ITEM_TPL = """
230 <item id='{id}' publisher='{publisher_jid}'> 301 <item id='{id}' publisher='{publisher_jid}'>
274 namespace=pubsub.NS_PUBSUB 345 namespace=pubsub.NS_PUBSUB
275 ) 346 )
276 for i in range(1, 5) 347 for i in range(1, 5)
277 ] 348 ]
278 349
350
279 async def mock_ap_get(url): 351 async def mock_ap_get(url):
280 return deepcopy(AP_REQUESTS[url]) 352 return deepcopy(AP_REQUESTS[url])
281 353
282 354
283 async def mock_treq_json(data): 355 async def mock_treq_json(data):
284 return dict(data) 356 return dict(data)
285 357
286 358
287 async def mock_getItems(*args, **kwargs): 359 async def mock_getItems(*args, **kwargs):
360 ret_items = kwargs.pop("ret_items", XMPP_ITEMS)
288 rsm_resp = rsm.RSMResponse( 361 rsm_resp = rsm.RSMResponse(
289 first=XMPP_ITEMS[0]["id"], 362 first=ret_items[0]["id"],
290 last=XMPP_ITEMS[-1]["id"], 363 last=ret_items[-1]["id"],
291 index=0, 364 index=0,
292 count=len(XMPP_ITEMS) 365 count=len(ret_items)
293 ) 366 )
294 return XMPP_ITEMS, {"rsm": rsm_resp.toDict(), "complete": True} 367 return ret_items, {"rsm": rsm_resp.toDict(), "complete": True}
295 368
296 369
297 @pytest.fixture(scope="session") 370 @pytest.fixture(scope="session")
298 def ap_gateway(host): 371 def ap_gateway(host):
299 gateway = plugin_comp_ap_gateway.APGateway(host) 372 gateway = plugin_comp_ap_gateway.APGateway(host)
300 gateway.initialised = True 373 gateway.initialised = True
374 gateway.isPubsub = AsyncMock()
375 gateway.isPubsub.return_value = False
301 client = MagicMock() 376 client = MagicMock()
302 client.jid = jid.JID("ap.test.example") 377 client.jid = jid.JID("ap.test.example")
303 client.host = "test.example" 378 client.host = "test.example"
304 gateway.client = client 379 gateway.client = client
305 gateway.local_only = True 380 gateway.local_only = True
306 gateway.public_url = "test.example" 381 gateway.public_url = PUBLIC_URL
307 gateway.ap_path = '_ap' 382 gateway.ap_path = '_ap'
308 gateway.base_ap_url = parse.urljoin( 383 gateway.base_ap_url = parse.urljoin(
309 f"https://{gateway.public_url}", 384 f"https://{gateway.public_url}",
310 f"{gateway.ap_path}/" 385 f"{gateway.ap_path}/"
311 ) 386 )
405 """AP requests are converted to pubsub""" 480 """AP requests are converted to pubsub"""
406 monkeypatch.setattr(plugin_comp_ap_gateway.treq, "get", mock_ap_get) 481 monkeypatch.setattr(plugin_comp_ap_gateway.treq, "get", mock_ap_get)
407 monkeypatch.setattr(plugin_comp_ap_gateway.treq, "json_content", mock_treq_json) 482 monkeypatch.setattr(plugin_comp_ap_gateway.treq, "json_content", mock_treq_json)
408 monkeypatch.setattr(ap_gateway, "apGet", mock_ap_get) 483 monkeypatch.setattr(ap_gateway, "apGet", mock_ap_get)
409 484
410 actor_data = await ap_gateway.getAPActorDataFromId(TEST_AP_ACCOUNT) 485 actor_data = await ap_gateway.getAPActorDataFromAccount(TEST_AP_ACCOUNT)
411 outbox = await ap_gateway.apGetObject(actor_data, "outbox") 486 outbox = await ap_gateway.apGetObject(actor_data, "outbox")
412 items, rsm_resp = await ap_gateway.getAPItems(outbox, 2) 487 items, rsm_resp = await ap_gateway.getAPItems(outbox, 2)
413 488
414 assert rsm_resp.count == 4 489 assert rsm_resp.count == 4
415 assert rsm_resp.index == 0 490 assert rsm_resp.index == 0
546 "request": request, 621 "request": request,
547 "account_jid": test_jid, 622 "account_jid": test_jid,
548 "node": None, 623 "node": None,
549 "ap_account": test_jid.full(), 624 "ap_account": test_jid.full(),
550 "ap_url": ap_url, 625 "ap_url": ap_url,
626 "signing_actor": None
551 } 627 }
552 if type_ == "outbox" and query_data: 628 if type_ == "outbox" and query_data:
553 kwargs["query_data"] = query_data 629 kwargs["query_data"] = query_data
630 # signing_actor is not used for page requests
631 del kwargs["signing_actor"]
554 return kwargs 632 return kwargs
555 633
556 @ed 634 @ed
557 async def test_pubsub_to_ap_conversion(self, ap_gateway, monkeypatch): 635 async def test_pubsub_to_ap_conversion(self, ap_gateway, monkeypatch):
558 """Pubsub nodes are converted to AP collections""" 636 """Pubsub nodes are converted to AP collections"""
584 assert first_item_obj["id"] == first_item["id"] 662 assert first_item_obj["id"] == first_item["id"]
585 assert first_item_obj["type"] == "Note" 663 assert first_item_obj["type"] == "Note"
586 assert first_item_obj["published"] == "2022-01-28T16:02:19Z" 664 assert first_item_obj["published"] == "2022-01-28T16:02:19Z"
587 assert first_item_obj["attributedTo"] == first_item["actor"] 665 assert first_item_obj["attributedTo"] == first_item["actor"]
588 assert first_item_obj["content"] == "<div><p>XMPP item 4</p></div>" 666 assert first_item_obj["content"] == "<div><p>XMPP item 4</p></div>"
589 assert first_item_obj["to"] == "https://www.w3.org/ns/activitystreams#Public" 667 assert first_item_obj["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
668
669 @ed
670 async def test_following_to_pps(self, ap_gateway, monkeypatch):
671 """AP following items are converted to Public Pubsub Subscription subscriptions"""
672 monkeypatch.setattr(plugin_comp_ap_gateway.treq, "get", mock_ap_get)
673 monkeypatch.setattr(plugin_comp_ap_gateway.treq, "json_content", mock_treq_json)
674 monkeypatch.setattr(ap_gateway, "apGet", mock_ap_get)
675
676 items, __ = await ap_gateway.pubsub_service.items(
677 jid.JID("toto@example.org"),
678 ap_gateway.getLocalJIDFromAccount(TEST_AP_ACCOUNT),
679 ap_gateway._pps.subscriptions_node,
680 None,
681 None,
682 None
683 )
684 assert len(items) == 2
685 for idx, entity in enumerate((
686 "local_user@test.example",
687 "ext_user\\40example.org@ap.test.example"
688 )):
689 subscription_elt = next(items[idx].elements(NS_PPS, "subscription"), None)
690 assert subscription_elt is not None
691 assert subscription_elt["node"] == ap_gateway._m.namespace
692 assert subscription_elt["service"] == entity
693
694 @ed
695 async def test_followers_to_pps(self, ap_gateway, monkeypatch):
696 """AP followers items are converted to Public Pubsub Subscription subscribers"""
697 monkeypatch.setattr(plugin_comp_ap_gateway.treq, "get", mock_ap_get)
698 monkeypatch.setattr(plugin_comp_ap_gateway.treq, "json_content", mock_treq_json)
699 monkeypatch.setattr(ap_gateway, "apGet", mock_ap_get)
700
701 items, __ = await ap_gateway.pubsub_service.items(
702 jid.JID("toto@example.org"),
703 ap_gateway.getLocalJIDFromAccount(TEST_AP_ACCOUNT),
704 ap_gateway._pps.getPublicSubscribersNode(ap_gateway._m.namespace),
705 None,
706 None,
707 None
708 )
709 assert len(items) == 2
710 for idx, entity in enumerate((
711 "local_user@test.example",
712 "ext_user\\40example.org@ap.test.example"
713 )):
714 subscriber_elt = next(items[idx].elements(NS_PPS, "subscriber"), None)
715 assert subscriber_elt is not None
716 assert subscriber_elt["jid"] == entity
717
718 @ed
719 async def test_pps_to_following(self, ap_gateway, monkeypatch):
720 """Public Pubsub Subscription subscriptions are converted to AP following items"""
721 subscriptions = [
722 pubsub.Item(
723 id="subscription_1",
724 payload = ap_gateway._pps.buildSubscriptionElt(
725 ap_gateway._m.namespace,
726 jid.JID("local_user@test.example")
727 )
728 ),
729 pubsub.Item(
730 id="subscription_2",
731 payload = ap_gateway._pps.buildSubscriptionElt(
732 ap_gateway._m.namespace,
733 jid.JID("ext_user\\40example.org@ap.test.example")
734 )
735 )
736 ]
737 monkeypatch.setattr(ap_gateway._p, "getItems", partial(
738 mock_getItems,
739 ret_items=subscriptions
740 ))
741 following = await ap_gateway.server.resource.APFollowingRequest(
742 **self.ap_request_params(ap_gateway, "following")
743 )
744 assert following["@context"] == "https://www.w3.org/ns/activitystreams"
745 assert following["id"] == "https://test.example/_ap/following/some_user%40test.example"
746 assert following["totalItems"] == len(subscriptions)
747 assert following["type"] == "OrderedCollection"
748 assert following.get("first")
749
750 first_page = following["first"]
751 assert first_page["type"] == "OrderedCollectionPage"
752 assert len(first_page["orderedItems"]) == len(subscriptions)
753 items = first_page["orderedItems"]
754 assert items == ['local_user@test.example', 'ext_user@example.org']
755
756 @ed
757 async def test_pps_to_followers(self, ap_gateway, monkeypatch):
758 """Public Pubsub Subscription subscribers are converted to AP followers"""
759 subscribers = [
760 pubsub.Item(
761 id="subscriber_1",
762 payload = ap_gateway._pps.buildSubscriberElt(
763 jid.JID("local_user@test.example")
764 )
765 ),
766 pubsub.Item(
767 id="subscriber_2",
768 payload = ap_gateway._pps.buildSubscriberElt(
769 jid.JID("ext_user\\40example.org@ap.test.example")
770 )
771 )
772 ]
773 monkeypatch.setattr(ap_gateway._p, "getItems", partial(
774 mock_getItems,
775 ret_items=subscribers
776 ))
777 followers = await ap_gateway.server.resource.APFollowersRequest(
778 **self.ap_request_params(ap_gateway, "followers")
779 )
780 assert followers["@context"] == "https://www.w3.org/ns/activitystreams"
781 assert followers["id"] == "https://test.example/_ap/followers/some_user%40test.example"
782 assert followers["totalItems"] == len(subscribers)
783 assert followers["type"] == "OrderedCollection"
784 assert followers.get("first")
785
786 first_page = followers["first"]
787 assert first_page["type"] == "OrderedCollectionPage"
788 assert len(first_page["orderedItems"]) == len(subscribers)
789 items = first_page["orderedItems"]
790 assert items == ['local_user@test.example', 'ext_user@example.org']