Mercurial > libervia-backend
view tests/unit/test_plugin_xep_0167.py @ 4242:8acf46ed7f36
frontends: remote control implementation:
This is the frontends common part of remote control implementation. It handle the creation
of WebRTC session, and management of inputs. For now the reception use freedesktop.org
Desktop portal, and works mostly with Wayland based Desktop Environments.
rel 436
author | Goffi <goffi@goffi.org> |
---|---|
date | Sat, 11 May 2024 13:52:43 +0200 |
parents | 716dd791be46 |
children | f1d0cde61af7 |
line wrap: on
line source
#!/usr/bin/env python3 # Libervia: an XMPP client # Copyright (C) 2009-2023 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 # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import base64 from unittest.mock import MagicMock, patch from pytest import fixture from pytest import raises from twisted.words.protocols.jabber import jid from libervia.backend.plugins.plugin_xep_0166 import XEP_0166 from libervia.backend.plugins.plugin_xep_0167 import XEP_0167, mapping from libervia.backend.plugins.plugin_xep_0167.constants import NS_JINGLE_RTP, NS_JINGLE_RTP_INFO from libervia.backend.tools import xml_tools from libervia.backend.tools.common import data_format @fixture(autouse=True) def no_application_register(monkeypatch): """Do not register the application in XEP-0166""" monkeypatch.setattr(XEP_0166, "register_application", lambda *a, **kw: None) class TestXEP0167Mapping: @fixture(scope="class", autouse=True) def set_mapping_host(self, host): mapping.host = host def test_senders_to_sdp(self): """Senders are mapped to SDP attribute""" assert mapping.senders_to_sdp("both", {"role": "initiator"}) == "a=sendrecv" assert mapping.senders_to_sdp("none", {"role": "initiator"}) == "a=inactive" assert mapping.senders_to_sdp("initiator", {"role": "initiator"}) == "a=sendonly" assert mapping.senders_to_sdp("responder", {"role": "initiator"}) == "a=recvonly" def test_generate_sdp_from_session(self): """SDP is correctly generated from session data""" session = { "local_jid": jid.JID("toto@example.org/test"), "metadata": {}, "contents": { "audio": { "application_data": { "media": "audio", "local_data": { "payload_types": { 96: { "name": "opus", "clockrate": 48000, "parameters": {"sprop-stereo": "1"}, } } }, }, "transport_data": { "local_ice_data": { "ufrag": "ufrag", "pwd": "pwd", "candidates": [ { "foundation": "1", "component_id": 1, "transport": "UDP", "priority": 1, "address": "10.0.0.1", "port": 12345, "type": "host", } ], } }, "senders": "both", } }, } expected_sdp = ( "v=0\r\n" f"o={base64.b64encode('toto@example.org/test'.encode()).decode()} 1 1 IN IP4 0.0.0.0\r\n" "s=-\r\n" "t=0 0\r\n" "a=msid-semantic:WMS *\r\n" "a=ice-options:trickle\r\n" "m=audio 9 UDP/TLS/RTP/SAVPF 96\r\n" "c=IN IP4 0.0.0.0\r\n" "a=mid:audio\r\n" "a=sendrecv\r\n" "a=rtpmap:96 opus/48000\r\n" "a=fmtp:96 sprop-stereo=1\r\n" "a=ice-ufrag:ufrag\r\n" "a=ice-pwd:pwd\r\n" "a=candidate:1 1 UDP 1 10.0.0.1 12345 typ host\r\n" ) assert mapping.generate_sdp_from_session(session, True) == expected_sdp def test_parse_sdp(self): """SDP is correctly parsed to session data""" sdp = ( "v=0\r\n" "o=toto@example.org/test 1 1 IN IP4 0.0.0.0\r\n" "s=-\r\n" "t=0 0\r\n" "a=sendrecv\r\n" "a=msid-semantic:WMS *\r\n" "m=audio 9999 UDP/TLS/RTP/SAVPF 96\r\n" "c=IN IP4 0.0.0.0\r\n" "a=mid:audio\r\n" "a=rtpmap:96 opus/48000\r\n" "a=fmtp:96 sprop-stereo=1\r\n" "a=ice-ufrag:ufrag\r\n" "a=ice-pwd:pwd\r\n" "a=candidate:1 1 UDP 1 10.0.0.1 12345 typ host\r\n" ) expected_session = { "audio": { "application_data": { "media": "audio", "payload_types": { 96: { "id": 96, "name": "opus", "clockrate": 48000, "parameters": {"sprop-stereo": "1"}, } }, }, "transport_data": { "port": 9999, "pwd": "pwd", "ufrag": "ufrag", "candidates": [ { "foundation": "1", "component_id": 1, "transport": "UDP", "priority": 1, "address": "10.0.0.1", "port": 12345, "type": "host", } ], }, "id": "audio", }, "metadata": {}, } assert mapping.parse_sdp(sdp) == expected_session def test_build_description(self): """<description> element is generated from media data""" session = {"metadata": {}} media_data = { "payload_types": { 96: { "channels": "2", "clockrate": "48000", "id": "96", "maxptime": "60", "name": "opus", "ptime": "20", "parameters": {"sprop-stereo": "1"}, } }, "bandwidth": "AS:40000", "rtcp-mux": True, "encryption": [ { "tag": "1", "crypto-suite": "AES_CM_128_HMAC_SHA1_80", "key-params": "inline:DPSKYRle84Ua2MjbScjadpCvFQH5Tutuls2N4/xx", "session-params": "", } ], } description_element = mapping.build_description("audio", media_data, session) # Assertions assert description_element.name == "description" assert description_element.uri == NS_JINGLE_RTP assert description_element["media"] == "audio" # Payload types payload_types = list(description_element.elements(NS_JINGLE_RTP, "payload-type")) assert len(payload_types) == 1 assert payload_types[0].name == "payload-type" assert payload_types[0]["id"] == "96" assert payload_types[0]["channels"] == "2" assert payload_types[0]["clockrate"] == "48000" assert payload_types[0]["maxptime"] == "60" assert payload_types[0]["name"] == "opus" assert payload_types[0]["ptime"] == "20" # Parameters parameters = list(payload_types[0].elements(NS_JINGLE_RTP, "parameter")) assert len(parameters) == 1 assert parameters[0].name == "parameter" assert parameters[0]["name"] == "sprop-stereo" assert parameters[0]["value"] == "1" # Bandwidth bandwidth = list(description_element.elements(NS_JINGLE_RTP, "bandwidth")) assert len(bandwidth) == 1 assert bandwidth[0]["type"] == "AS:40000" # RTCP-mux rtcp_mux = list(description_element.elements(NS_JINGLE_RTP, "rtcp-mux")) assert len(rtcp_mux) == 1 # Encryption encryption = list(description_element.elements(NS_JINGLE_RTP, "encryption")) assert len(encryption) == 1 assert encryption[0]["required"] == "1" crypto = list(encryption[0].elements("crypto")) assert len(crypto) == 1 assert crypto[0]["tag"] == "1" assert crypto[0]["crypto-suite"] == "AES_CM_128_HMAC_SHA1_80" assert ( crypto[0]["key-params"] == "inline:DPSKYRle84Ua2MjbScjadpCvFQH5Tutuls2N4/xx" ) assert crypto[0]["session-params"] == "" def test_parse_description(self): """Parsing <description> to a dict is successful""" description_element = xml_tools.parse( """ <description xmlns="urn:xmpp:jingle:apps:rtp:1" media="audio"> <payload-type id="96" channels="2" clockrate="48000" maxptime="60" name="opus" ptime="20"> <parameter name="sprop-stereo" value="1" /> </payload-type> <bandwidth type="AS:40000" /> <rtcp-mux /> <encryption required="1"> <crypto tag="1" crypto-suite="AES_CM_128_HMAC_SHA1_80" key-params="inline:DPSKYRle84Ua2MjbScjadpCvFQH5Tutuls2N4/xx" session-params="" /> </encryption> </description> """ ) parsed_data = mapping.parse_description(description_element) # Assertions assert parsed_data["payload_types"] == { 96: { "channels": "2", "clockrate": "48000", "maxptime": "60", "name": "opus", "ptime": "20", "parameters": {"sprop-stereo": "1"}, } } assert parsed_data["bandwidth"] == "AS:40000" assert parsed_data["rtcp-mux"] is True assert parsed_data["encryption_required"] is True assert parsed_data["encryption"] == [ { "tag": "1", "crypto-suite": "AES_CM_128_HMAC_SHA1_80", "key-params": "inline:DPSKYRle84Ua2MjbScjadpCvFQH5Tutuls2N4/xx", "session-params": "", } ] class TestXEP0167: def test_jingle_session_info(self, host, client): """Bridge's call_info method is called with correct parameters.""" xep_0167 = XEP_0167(host) session = {"id": "123"} mock_call_info = MagicMock() host.bridge.call_info = mock_call_info jingle_elt = xml_tools.parse( """ <jingle xmlns='urn:xmpp:jingle:1' action='session-info' initiator='client1@example.org' sid='a73sjjvkla37jfea'> <mute xmlns="urn:xmpp:jingle:apps:rtp:info:1" name="mute_name"/> </jingle> """ ) xep_0167.jingle_session_info(client, "mute", session, "content_name", jingle_elt) mock_call_info.assert_called_with( session["id"], "mute", data_format.serialise({"name": "mute_name"}), client.profile, ) def test_jingle_session_info_invalid_actions(self, host, client): """When receiving invalid actions, no further action is taken.""" xep_0167 = XEP_0167(host) session = {"id": "123"} mock_call_info = MagicMock() host.bridge.call_info = mock_call_info jingle_elt = xml_tools.parse( """ <jingle xmlns='urn:xmpp:jingle:1' action='session-info' initiator='client1@example.org' sid='a73sjjvkla37jfea'> <invalid xmlns="urn:xmpp:jingle:apps:rtp:info:1" name="invalid_name"/> </jingle> """ ) xep_0167.jingle_session_info( client, "invalid", session, "content_name", jingle_elt ) mock_call_info.assert_not_called() def test_send_info(self, host, client): """A jingle element with the correct info is created and sent.""" xep_0167 = XEP_0167(host) session_id = "123" extra = {"name": "test"} iq_elt = xml_tools.parse( """ <iq from='client1@example.org' id='yh3gr714' to='client2@example.net' type='set'> <jingle xmlns='urn:xmpp:jingle:1' action='session-info' initiator='client1@example.org' sid='a73sjjvkla37jfea'> <active xmlns='urn:xmpp:jingle:apps:rtp:info:1'/> </jingle> </iq> """ ) jingle_elt = iq_elt.firstChildElement() mock_send = MagicMock() iq_elt.send = mock_send with patch.object( xep_0167._j, "build_session_info", return_value=(iq_elt, jingle_elt) ): xep_0167.send_info(client, session_id, "mute", extra) info_elt = jingle_elt.firstChildElement() assert info_elt.name == "active" assert info_elt.uri == NS_JINGLE_RTP_INFO mock_send.assert_called() def test_send_info_invalid_actions(self, host, client): """When trying to send invalid actions, an error is raised.""" xep_0167 = XEP_0167(host) session_id = "123" extra = {"name": "test"} with raises(ValueError, match="Unkown info type 'invalid_action'"): xep_0167.send_info(client, session_id, "invalid_action", extra)