diff --git a/libervia/backend/core/xmpp.py b/libervia/backend/core/xmpp.py index 2ff93270..efd5ccf7 100644 --- a/libervia/backend/core/xmpp.py +++ b/libervia/backend/core/xmpp.py @@ -24,7 +24,7 @@ import mimetypes from pathlib import Path import sys import time -from typing import Callable, Dict, Optional, Tuple, cast +from typing import Any, Callable, Dict, Optional, Tuple, cast from urllib.parse import unquote, urlparse import uuid @@ -741,7 +741,7 @@ class SatXMPPEntity(core_types.SatXMPPEntity): message: dict[str, str], subject: dict[str, str] | None = None, mess_type: str = "auto", - extra: dict[str, str] | None = None, + extra: dict[str, Any] | None = None, uid: str | None = None, no_trigger: bool = False, ) -> defer.Deferred: diff --git a/libervia/backend/plugins/plugin_comp_email_gateway/pubsub_service.py b/libervia/backend/plugins/plugin_comp_email_gateway/pubsub_service.py index a505caa6..e8d29f28 100644 --- a/libervia/backend/plugins/plugin_comp_email_gateway/pubsub_service.py +++ b/libervia/backend/plugins/plugin_comp_email_gateway/pubsub_service.py @@ -83,7 +83,7 @@ class EmailGWPubsubResource(pubsub.PubSubResource): return defer.ensureDeferred(self.get_nodes(requestor, service, nodeIdentifier)) async def get_nodes( - self, requestor: jid.JID, service: jid.JID, node_id: str + self, requestor: jid.JID, service: jid.JID, node_id: str|None ) -> list[str]: if node_id: # we only return result for requests without node. diff --git a/libervia/backend/plugins/plugin_comp_file_sharing.py b/libervia/backend/plugins/plugin_comp_file_sharing.py index abc1f491..0eab38d1 100644 --- a/libervia/backend/plugins/plugin_comp_file_sharing.py +++ b/libervia/backend/plugins/plugin_comp_file_sharing.py @@ -119,9 +119,7 @@ class QuotasSettings(ConfigOptions): return utils.parse_size(value) return value - @field_validator( - "jids", mode="before", json_schema_input_type=dict[str, int | str] - ) + @field_validator("jids", mode="before", json_schema_input_type=dict[str, int | str]) @classmethod def parse_jid_quotas(cls, value: Any) -> Any: """Parse string sizes to int for ``jids``.""" diff --git a/libervia/backend/plugins/plugin_pubsub_cache.py b/libervia/backend/plugins/plugin_pubsub_cache.py index 6c1f1c12..e1e8ab3f 100644 --- a/libervia/backend/plugins/plugin_pubsub_cache.py +++ b/libervia/backend/plugins/plugin_pubsub_cache.py @@ -66,17 +66,13 @@ class PubsubCache: def __init__(self, host): log.info(_("PubSub Cache initialization")) - strategy = G.config.common.pubsub_cache_strategy - if strategy == "no_cache": + if (strategy := G.config.common.pubsub_cache_strategy) == "no_cache": log.info( _( "Pubsub cache won't be used due to pubsub_cache_strategy={value} " "setting." ).format(value=repr(strategy)) ) - self.use_cache = False - else: - self.use_cache = True self.host = host self._p = host.plugins["XEP-0060"] self.analysers = {} @@ -592,7 +588,10 @@ class PubsubCache: rsm_request: Optional[rsm.RSMRequest], extra: dict, ) -> Tuple[bool, Optional[Tuple[List[dict], dict]]]: - if not self.use_cache: + # Check if cache is disabled by configuration (checking at runtime to handle + # dynamic config changes) + from pudb import set_trace; set_trace() + if G.config.common.pubsub_cache_strategy == "no_cache": log.debug("cache disabled in settings") return True, None if extra.get(C.KEY_USE_CACHE) == False: diff --git a/libervia/backend/plugins/plugin_xep_0471.py b/libervia/backend/plugins/plugin_xep_0471.py index 1b42e1dd..91f9c129 100644 --- a/libervia/backend/plugins/plugin_xep_0471.py +++ b/libervia/backend/plugins/plugin_xep_0471.py @@ -67,9 +67,9 @@ PLUGIN_INFO = { NS_EVENT = "org.salut-a-toi.event:0" NS_EVENTS_BASE: Final = "urn:xmpp:events" -NS_EVENTS: Final = "{NS_EVENTS_BASE}:0" -NS_RSVP: Final = "{NS_EVENTS_BASE}:rsvp:0" -NS_EXTRA: Final = "{NS_EVENTS_BASE}:extra:0" +NS_EVENTS: Final = f"{NS_EVENTS_BASE}:0" +NS_RSVP: Final = f"{NS_EVENTS_BASE}:rsvp:0" +NS_EXTRA: Final = f"{NS_EVENTS_BASE}:extra:0" class XEP_0471: diff --git a/libervia/backend/tools/config.py b/libervia/backend/tools/config.py index 98569e14..808cd8d0 100644 --- a/libervia/backend/tools/config.py +++ b/libervia/backend/tools/config.py @@ -105,9 +105,7 @@ class DynamicSettingsBase(ConfigOptions): _sub_key: str | None = None @classmethod - def _init_subclass( - cls, /, key: str, plugin_info: dict | None = None - ) -> None: + def _init_subclass(cls, /, key: str, plugin_info: dict | None = None) -> None: """Initialize the sublass, and register it in DynamicSettings. @param key: name of the field which will be used to store those settings. @@ -475,7 +473,7 @@ def parse_main_conf(log_filenames: bool = False, ignore_cache=False) -> Settings for sub_key, settings_type in settings.items() }, ), - __config__ = ConfigDict(extra="forbid") + __config__=ConfigDict(extra="forbid"), ) dynamic_settings_kw[key] = ( NewSettingsModel, diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index e8a2cad2..15198f35 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # Libervia: an XMPP client -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) +# Copyright (C) 2009-2025 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 @@ -21,6 +21,8 @@ from unittest.mock import MagicMock, AsyncMock from pytest import fixture from twisted.internet import defer from twisted.words.protocols.jabber import jid +from libervia.backend import G +from wokkel.data_form import NS_X_DATA from libervia.backend.core.main import LiberviaBackend from libervia.backend.tools import async_trigger as trigger from libervia.backend.core import xmpp @@ -42,6 +44,7 @@ def storage(): class MockLiberviaBackend(LiberviaBackend): def __init__(self, bridge, storage): + G.set_host(self) self._cb_map = {} self._menus = {} self._menus_paths = {} @@ -51,7 +54,7 @@ class MockLiberviaBackend(LiberviaBackend): # map for short name to whole namespace, # extended by plugins with register_namespace self.ns_map = { - "x-data": xmpp.NS_X_DATA, + "x-data": NS_X_DATA, "disco#info": xmpp.NS_DISCO_INFO, } self.memory = MagicMock() @@ -73,36 +76,11 @@ class MockLiberviaBackend(LiberviaBackend): def get_test_config(self, section, name, default=None): return self._test_config.get((section or None, name), default) - def set_test_config(self, section, name, value): - self._test_config[(section or None, name)] = value - - def clear_test_config(self): - self._test_config.clear() - def register_namespace(self, short_name, namespace): # Plugin classes may be instantiated several times in tests, so we do nothing in # this method to avoid ConflictError. pass - @contextmanager - def use_option_and_reload(self, section, name, value): - self.set_test_config(section, name, value) - self.reload_plugins() - try: - yield self - finally: - self.clear_test_config() - self.reload_plugins() - - def reload_plugins(self): - self.plugins.clear() - self.trigger._TriggerManager__triggers.clear() - self.ns_map = { - "x-data": xmpp.NS_X_DATA, - "disco#info": xmpp.NS_DISCO_INFO, - } - self._import_plugins() - def _init(self): pass diff --git a/tests/unit/frontends/test_webrtc.py b/tests/unit/frontends/test_webrtc.py index 34f577e8..b77b6486 100644 --- a/tests/unit/frontends/test_webrtc.py +++ b/tests/unit/frontends/test_webrtc.py @@ -24,6 +24,8 @@ try: except ImportError: pytest.skip("Gst not available.", allow_module_level=True) +from pytest_twisted import ensureDeferred as ed + from libervia.backend.core import exceptions from libervia.backend.tools.common import data_format from libervia.frontends.tools import webrtc as webrtc_mod @@ -95,7 +97,7 @@ class TestWebRtc: assert webrtc.webrtcbin.emit.call_count == 1 assert webrtc.webrtcbin.emit.call_args[0][0] == "set-remote-description" - @pytest.mark.asyncio + @ed async def test_answer_call(self, webrtc, monkeypatch): """The method correctly answers an incoming call.""" mock_setup_call = AsyncMock() @@ -128,12 +130,11 @@ class TestWebRtc: mock_pad.has_current_caps.return_value = True mock_pad.get_current_caps.return_value = mock_caps - mock_caps.__len__.return_value = 1 - mock_caps.__getitem__.return_value = mock_structure + mock_caps.get_structure.return_value = mock_structure mock_structure.get_name.return_value = "video/x-h264" # We use non-standard resolution as example to trigger the workaround - mock_structure.get_int.side_effect = lambda x: MagicMock( - value=990 if x == "width" else 557 + mock_structure.get_int = MagicMock( + side_effect=lambda x: (0, 990) if x == "width" else (0, 557) ) webrtc.on_remote_decodebin_stream(None, mock_pad) @@ -153,8 +154,7 @@ class TestWebRtc: mock_pad.has_current_caps.return_value = True mock_pad.get_current_caps.return_value = mock_caps - mock_caps.__len__.return_value = 1 - mock_caps.__getitem__.return_value = mock_structure + mock_caps.get_structure.return_value = mock_structure mock_structure.get_name.return_value = "audio/x-raw" webrtc.on_remote_decodebin_stream(None, mock_pad) @@ -163,7 +163,7 @@ class TestWebRtc: mock_pad.link.assert_called() @pytest.mark.skipif(Gst is None, reason="GStreamer is not available") - @pytest.mark.asyncio + @ed async def test_setup_call_correct_role(self, host, webrtc, monkeypatch): """Roles are set in setup_call.""" monkeypatch.setattr(Gst, "parse_launch", MagicMock()) @@ -181,7 +181,7 @@ class TestWebRtc: with pytest.raises(AssertionError): await webrtc.setup_call("invalid_role") - @pytest.mark.asyncio + @ed async def test_setup_call_test_mode(self, host, webrtc, monkeypatch): """Test mode use fake video and audio in setup_call.""" monkeypatch.setattr(data_format, "deserialise", MagicMock(return_value=[])) @@ -190,7 +190,7 @@ class TestWebRtc: assert "videotestsrc" in webrtc.gst_pipe_desc assert "audiotestsrc" in webrtc.gst_pipe_desc - @pytest.mark.asyncio + @ed async def test_setup_call_normal_mode(self, host, webrtc, monkeypatch): """Normal mode use real video and audio in setup_call.""" monkeypatch.setattr(data_format, "deserialise", MagicMock(return_value=[])) @@ -200,7 +200,7 @@ class TestWebRtc: assert "pulsesrc" in webrtc.gst_pipe_desc @pytest.mark.skipif(Gst is None, reason="GStreamer is not available") - @pytest.mark.asyncio + @ed async def test_setup_call_with_stun_and_turn(self, host, webrtc, monkeypatch): """STUN and TURN server configurations are done in setup_call.""" mock_pipeline = MagicMock() @@ -244,7 +244,7 @@ class TestWebRtc: ) @pytest.mark.skipif(Gst is None, reason="GStreamer is not available") - @pytest.mark.asyncio + @ed async def test_setup_call_gstreamer_pipeline_failure(self, webrtc, monkeypatch): """Test setup_call method handling Gstreamer pipeline failure.""" monkeypatch.setattr(Gst, "parse_launch", lambda _: None) diff --git a/tests/unit/test_ap-gateway.py b/tests/unit/test_ap-gateway.py index 7ecf023c..54d65190 100644 --- a/tests/unit/test_ap-gateway.py +++ b/tests/unit/test_ap-gateway.py @@ -42,7 +42,7 @@ from libervia.backend.plugins import plugin_comp_ap_gateway from libervia.backend.plugins.plugin_comp_ap_gateway import constants as ap_const from libervia.backend.plugins.plugin_comp_ap_gateway import TYPE_ACTOR from libervia.backend.plugins.plugin_comp_ap_gateway.http_server import HTTPServer -from libervia.backend.plugins.plugin_xep_0277 import NS_ATOM +from libervia.backend.plugins.plugin_xep_0277 import NS_ATOM, MbData from libervia.backend.plugins.plugin_xep_0424 import NS_MESSAGE_RETRACT from libervia.backend.plugins.plugin_xep_0465 import NS_PPS from libervia.backend.tools import xml_tools @@ -1238,12 +1238,12 @@ class TestActivityPubGateway: monkeypatch.setattr(plugin_comp_ap_gateway.treq, "json_content", mock_treq_json) monkeypatch.setattr(ap_gateway, "ap_get", mock_ap_get) - mb_data = { - "author_jid": TEST_JID.full(), - "content": f"mention of @{TEST_AP_ACCOUNT}", - "service": TEST_JID.full(), - "node": ap_gateway._m.namespace, - } + mb_data = MbData( + author_jid=TEST_JID, + content=f"mention of @{TEST_AP_ACCOUNT}", + service=TEST_JID, + node=ap_gateway._m.namespace, + ) ap_item = await ap_gateway.mb_data_2_ap_item( ap_gateway.client, mb_data, public=True ) @@ -1266,12 +1266,12 @@ class TestActivityPubGateway: monkeypatch.setattr(plugin_comp_ap_gateway.treq, "json_content", mock_treq_json) monkeypatch.setattr(ap_gateway, "ap_get", mock_ap_get) - mb_data = { - "author_jid": TEST_JID.full(), - "content": f"mention of @{TEST_AP_ACCOUNT}", - "service": TEST_JID.full(), - "node": ap_gateway._m.namespace, - } + mb_data = MbData( + author_jid=TEST_JID, + content=f"mention of @{TEST_AP_ACCOUNT}", + service=TEST_JID, + node=ap_gateway._m.namespace, + ) ap_item = await ap_gateway.mb_data_2_ap_item( ap_gateway.client, mb_data, public=False ) @@ -1448,20 +1448,17 @@ class TestActivityPubGateway: assert cache_pubsub_items.called # the microblog data put in cache correspond to the item sent to subscribers __, __, __, [mb_data] = cache_pubsub_items.call_args.args - extra = mb_data["extra"] - assert "repeated" in extra - repeated = extra["repeated"] - assert ( - repeated["by"] - == ap_gateway.get_local_jid_from_account(TEST_AP_ACCOUNT).full() - ) + extra = mb_data.extra + repeated = extra.repeated + assert repeated is not None + assert repeated.by == ap_gateway.get_local_jid_from_account(TEST_AP_ACCOUNT) xmpp_item_xmpp_url = xmpp_uri.build_xmpp_uri( "pubsub", path=TEST_JID.full(), node=ap_gateway._m.namespace, item=xmpp_item["id"], ) - assert repeated["uri"] == xmpp_item_xmpp_url + assert repeated.uri == xmpp_item_xmpp_url @ed async def test_xmpp_attachment_noticed_to_ap_like(self, ap_gateway, monkeypatch): diff --git a/tests/unit/test_email_gateway.py b/tests/unit/test_email_gateway.py index 747dfa2b..8ff652c1 100644 --- a/tests/unit/test_email_gateway.py +++ b/tests/unit/test_email_gateway.py @@ -51,6 +51,19 @@ def email_gw(host): return email_gw +TEST_CREDENTIALS_KW = { + "user_name": "User Name", + "smtp_host": "smtp.example.org", + "smtp_port": 587, + "smtp_username": "user", + "smtp_password": "password", + "imap_host": "imap.example.org", + "imap_port": 993, + "imap_username": "user", + "imap_password": "password", +} + + class TestEmailGatewayComponent: def test_jid_to_email_gateway_jid(self, email_gw): @@ -58,7 +71,7 @@ class TestEmailGatewayComponent: client = MagicMock() client.jid = jid.JID("gateway.example.org") address_jid = jid.JID(r"user\40some-email-domain.example@gateway.example.org") - credentials = {"user_email": "user@example.org"} + credentials = Credentials(user_email="user@example.org", **TEST_CREDENTIALS_KW) result = email_gw.jid_to_email(client, address_jid, credentials) assert result == "user@some-email-domain.example" @@ -67,7 +80,7 @@ class TestEmailGatewayComponent: client = MagicMock() client.jid = jid.JID("gateway.example.org") address_jid = jid.JID("external@example.org") - credentials = {"user_email": "user@example.org"} + credentials = Credentials(user_email="user@example.org", **TEST_CREDENTIALS_KW) result = email_gw.jid_to_email(client, address_jid, credentials) assert result == '"xmpp:external@example.org" ' @@ -129,7 +142,7 @@ class TestEmailGatewayComponent: to_jid = jid.JID("gw-user@example.org") user_email = "user@example.org" - user_data = UserData(Credentials({"user_email": user_email})) + user_data = UserData(Credentials(user_email=user_email, **TEST_CREDENTIALS_KW)) await email_gw.on_new_email(user_data, to_jid, email) client.sendMessage.assert_called_once() @@ -138,7 +151,7 @@ class TestEmailGatewayComponent: assert call_args[1] == {"": "Hello, world!\n"} assert call_args[2] == None client.sendMessage.assert_called_once_with( - to_jid, {"": "Hello, world!\n"}, None, extra={} + to_jid, {"": "Hello, world!\n"}, None, extra={}, uid=None ) @ed @@ -159,7 +172,7 @@ class TestEmailGatewayComponent: user_jid = jid.JID("gw-user@example.org") user_email = "user@example.org" - user_data = UserData(Credentials({"user_email": user_email})) + user_data = UserData(Credentials(user_email=user_email, **TEST_CREDENTIALS_KW)) await email_gw.on_new_email(user_data, user_jid, email) client.sendMessage.assert_called_once_with( @@ -183,6 +196,7 @@ class TestEmailGatewayComponent: ], } }, + uid=None ) @ed @@ -192,14 +206,10 @@ class TestEmailGatewayComponent: email_gw.client.jid = jid.JID("gateway.example.org") email_gw.storage = MagicMock() email_gw.storage.get = AsyncMock( - return_value={ - "user_email": "user@example.org", - "user_name": "Sender Name", - "smtp_host": "smtp.example.org", - "smtp_port": "587", - "smtp_username": "sender", - "smtp_password": "password", - } + return_value=Credentials( + user_email="user@example.org", + **TEST_CREDENTIALS_KW + ) ) from_jid = jid.JID("user@example.org") @@ -248,7 +258,7 @@ class TestEmailGatewayComponent: to_jid = jid.JID("gw-user@example.org") user_email = "user@example.org" - user_data = UserData(Credentials({"user_email": user_email})) + user_data = UserData(Credentials(user_email=user_email, **TEST_CREDENTIALS_KW)) await email_gw.on_new_email(user_data, to_jid, email) client.sendMessage.assert_called_once_with( @@ -256,6 +266,7 @@ class TestEmailGatewayComponent: {"": "Hello, world!\n"}, None, extra={"headers": {"keywords": "test, example", "urgency": "high"}}, + uid=None ) @@ -271,13 +282,15 @@ class TestPubsubService: def pubsub_resource(self, pubsub_service): return pubsub_service.resource - def test_getNodes(self, pubsub_resource, client): + @ed + async def test_getNodes(self, pubsub_resource, client): """XEP-0498 well-known node is returned.""" requestor = client.jid service = client.pubsub_service - nodeIdentifier = "test_node" - result = pubsub_resource.getNodes(requestor, service, nodeIdentifier) - assert result.result == [pubsub_resource._pfs.namespace] + # node_id must be None to get the nodes. + node_id = None + result = await pubsub_resource.getNodes(requestor, service, node_id) + assert result == [pubsub_resource._pfs.namespace] @ed async def test_items(self, pubsub_resource, client, host): diff --git a/tests/unit/test_plugin_xep_0176.py b/tests/unit/test_plugin_xep_0176.py index 726a6933..3aa5cbee 100644 --- a/tests/unit/test_plugin_xep_0176.py +++ b/tests/unit/test_plugin_xep_0176.py @@ -225,7 +225,7 @@ class TestXEP0176: transport_elt = await xep_0176.jingle_session_init(client, session, content_name) expected_transport_elt = xep_0176.build_transport( - session["contents"][content_name]["transport_data"]["local_ice_data"] + session["contents"][content_name]["transport_data"]["local_ice_data"], add_trickle=True ) assert transport_elt.toXml() == expected_transport_elt.toXml() @@ -245,7 +245,7 @@ class TestXEP0176: ) expected_transport_elt = xep_0176.build_transport( - session["contents"][content_name]["transport_data"]["local_ice_data"] + session["contents"][content_name]["transport_data"]["local_ice_data"], add_trickle=True ) assert returned_transport_elt.toXml() == expected_transport_elt.toXml() diff --git a/tests/unit/test_pubsub-cache.py b/tests/unit/test_pubsub-cache.py index fe423ffc..73afdac7 100644 --- a/tests/unit/test_pubsub-cache.py +++ b/tests/unit/test_pubsub-cache.py @@ -24,7 +24,6 @@ from libervia.backend.core.constants import Const as C class TestPubsubCache: - @ed async def test_cache_is_used_transparently(self, host, client): """Cache is used when a pubsub get_items operation is done""" @@ -61,12 +60,16 @@ class TestPubsubCache: @ed async def test_cache_is_not_used_when_no_cache(self, host, client): """Cache is skipped when 'pubsub_cache_strategy' is set to 'no_cache'""" - with host.use_option_and_reload(None, "pubsub_cache_strategy", "no_cache"): + with patch("libervia.backend.plugins.plugin_pubsub_cache.G") as mock_G: + mock_G.config.common.pubsub_cache_strategy = "no_cache" + items_ret = defer.Deferred() items_ret.callback(([], {})) client.pubsub_client.items = MagicMock(return_value=items_ret) host.memory.storage.get_pubsub_node.return_value = None - host.memory.storage.set_pubsub_node.return_value = PubsubNode(sync_state=None) + host.memory.storage.set_pubsub_node.return_value = PubsubNode( + sync_state=None + ) with patch.object(host.plugins["PUBSUB_CACHE"], "cache_node") as cache_node: await host.plugins["XEP-0060"].get_items( client,