comparison tests/unit/test_plugin_xep_0167.py @ 4057:e807a5434f82

tests (units): tests for plugin XEP-0167: fix 420
author Goffi <goffi@goffi.org>
date Mon, 29 May 2023 13:38:11 +0200
parents
children 4b842c1fb686
comparison
equal deleted inserted replaced
4056:1c4f4aa36d98 4057:e807a5434f82
1 #!/usr/bin/env python3
2
3 # Libervia: an XMPP client
4 # Copyright (C) 2009-2023 Jérôme Poisson (goffi@goffi.org)
5
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Affero General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU Affero General Public License for more details.
15
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/>.
18
19 import base64
20 from unittest.mock import MagicMock, patch
21
22 from pytest import fixture
23 from pytest import raises
24 from twisted.words.protocols.jabber import jid
25
26 from sat.plugins.plugin_xep_0166 import XEP_0166
27 from sat.plugins.plugin_xep_0167 import XEP_0167, mapping
28 from sat.plugins.plugin_xep_0167.constants import NS_JINGLE_RTP, NS_JINGLE_RTP_INFO
29 from sat.tools import xml_tools
30 from sat.tools.common import data_format
31
32
33 @fixture(autouse=True)
34 def no_application_register(monkeypatch):
35 """Do not register the application in XEP-0166"""
36 monkeypatch.setattr(XEP_0166, "register_application", lambda *a, **kw: None)
37
38
39 class TestXEP0167Mapping:
40 @fixture(scope="class", autouse=True)
41 def set_mapping_host(self, host):
42 mapping.host = host
43
44 def test_senders_to_sdp(self):
45 """Senders are mapped to SDP attribute"""
46 assert mapping.senders_to_sdp("both", {"role": "initiator"}) == "a=sendrecv"
47 assert mapping.senders_to_sdp("none", {"role": "initiator"}) == "a=inactive"
48 assert mapping.senders_to_sdp("initiator", {"role": "initiator"}) == "a=sendonly"
49 assert mapping.senders_to_sdp("responder", {"role": "initiator"}) == "a=recvonly"
50
51 def test_generate_sdp_from_session(self):
52 """SDP is correctly generated from session data"""
53 session = {
54 "local_jid": jid.JID("toto@example.org/test"),
55 "metadata": {},
56 "contents": {
57 "audio": {
58 "application_data": {
59 "media": "audio",
60 "local_data": {
61 "payload_types": {
62 96: {
63 "name": "opus",
64 "clockrate": 48000,
65 "parameters": {"sprop-stereo": "1"},
66 }
67 }
68 },
69 },
70 "transport_data": {
71 "local_ice_data": {
72 "ufrag": "ufrag",
73 "pwd": "pwd",
74 "candidates": [
75 {
76 "foundation": "1",
77 "component_id": 1,
78 "transport": "UDP",
79 "priority": 1,
80 "address": "10.0.0.1",
81 "port": 12345,
82 "type": "host",
83 }
84 ],
85 }
86 },
87 "senders": "both",
88 }
89 },
90 }
91
92 expected_sdp = (
93 "v=0\r\n"
94 f"o={base64.b64encode('toto@example.org/test'.encode()).decode()} 1 1 IN IP4 0.0.0.0\r\n"
95 "s=-\r\n"
96 "t=0 0\r\n"
97 "a=sendrecv\r\n"
98 "a=msid-semantic:WMS *\r\n"
99 "m=audio 9999 UDP/TLS/RTP/SAVPF 96\r\n"
100 "c=IN IP4 0.0.0.0\r\n"
101 "a=mid:audio\r\n"
102 "a=rtpmap:96 opus/48000\r\n"
103 "a=fmtp:96 sprop-stereo=1\r\n"
104 "a=ice-ufrag:ufrag\r\n"
105 "a=ice-pwd:pwd\r\n"
106 "a=candidate:1 1 UDP 1 10.0.0.1 12345 typ host\r\n"
107 )
108
109 assert mapping.generate_sdp_from_session(session, True) == expected_sdp
110
111 def test_parse_sdp(self):
112 """SDP is correctly parsed to session data"""
113 sdp = (
114 "v=0\r\n"
115 "o=toto@example.org/test 1 1 IN IP4 0.0.0.0\r\n"
116 "s=-\r\n"
117 "t=0 0\r\n"
118 "a=sendrecv\r\n"
119 "a=msid-semantic:WMS *\r\n"
120 "m=audio 9999 UDP/TLS/RTP/SAVPF 96\r\n"
121 "c=IN IP4 0.0.0.0\r\n"
122 "a=mid:audio\r\n"
123 "a=rtpmap:96 opus/48000\r\n"
124 "a=fmtp:96 sprop-stereo=1\r\n"
125 "a=ice-ufrag:ufrag\r\n"
126 "a=ice-pwd:pwd\r\n"
127 "a=candidate:1 1 UDP 1 10.0.0.1 12345 typ host\r\n"
128 )
129
130 expected_session = {
131 "audio": {
132 "application_data": {
133 "media": "audio",
134 "payload_types": {
135 96: {
136 "id": 96,
137 "name": "opus",
138 "clockrate": 48000,
139 "parameters": {"sprop-stereo": "1"},
140 }
141 },
142 },
143 "transport_data": {
144 "port": 9999,
145 "pwd": "pwd",
146 "ufrag": "ufrag",
147 "candidates": [
148 {
149 "foundation": "1",
150 "component_id": 1,
151 "transport": "UDP",
152 "priority": 1,
153 "address": "10.0.0.1",
154 "port": 12345,
155 "type": "host",
156 }
157 ],
158 },
159 "id": "audio",
160 },
161 "metadata": {},
162 }
163
164 assert mapping.parse_sdp(sdp) == expected_session
165
166 def test_build_description(self):
167 """<description> element is generated from media data"""
168 session = {"metadata": {}}
169
170 media_data = {
171 "payload_types": {
172 96: {
173 "channels": "2",
174 "clockrate": "48000",
175 "id": "96",
176 "maxptime": "60",
177 "name": "opus",
178 "ptime": "20",
179 "parameters": {"sprop-stereo": "1"},
180 }
181 },
182 "bandwidth": "AS:40000",
183 "rtcp-mux": True,
184 "encryption": [
185 {
186 "tag": "1",
187 "crypto-suite": "AES_CM_128_HMAC_SHA1_80",
188 "key-params": "inline:DPSKYRle84Ua2MjbScjadpCvFQH5Tutuls2N4/xx",
189 "session-params": "",
190 }
191 ],
192 }
193
194 description_element = mapping.build_description("audio", media_data, session)
195
196 # Assertions
197 assert description_element.name == "description"
198 assert description_element.uri == NS_JINGLE_RTP
199 assert description_element["media"] == "audio"
200
201 # Payload types
202 payload_types = list(description_element.elements(NS_JINGLE_RTP, "payload-type"))
203 assert len(payload_types) == 1
204 assert payload_types[0].name == "payload-type"
205 assert payload_types[0]["id"] == "96"
206 assert payload_types[0]["channels"] == "2"
207 assert payload_types[0]["clockrate"] == "48000"
208 assert payload_types[0]["maxptime"] == "60"
209 assert payload_types[0]["name"] == "opus"
210 assert payload_types[0]["ptime"] == "20"
211
212 # Parameters
213 parameters = list(payload_types[0].elements(NS_JINGLE_RTP, "parameter"))
214 assert len(parameters) == 1
215 assert parameters[0].name == "parameter"
216 assert parameters[0]["name"] == "sprop-stereo"
217 assert parameters[0]["value"] == "1"
218
219 # Bandwidth
220 bandwidth = list(description_element.elements(NS_JINGLE_RTP, "bandwidth"))
221 assert len(bandwidth) == 1
222 assert bandwidth[0]["type"] == "AS:40000"
223
224 # RTCP-mux
225 rtcp_mux = list(description_element.elements(NS_JINGLE_RTP, "rtcp-mux"))
226 assert len(rtcp_mux) == 1
227
228 # Encryption
229 encryption = list(description_element.elements(NS_JINGLE_RTP, "encryption"))
230 assert len(encryption) == 1
231 assert encryption[0]["required"] == "1"
232 crypto = list(encryption[0].elements("crypto"))
233 assert len(crypto) == 1
234 assert crypto[0]["tag"] == "1"
235 assert crypto[0]["crypto-suite"] == "AES_CM_128_HMAC_SHA1_80"
236 assert (
237 crypto[0]["key-params"] == "inline:DPSKYRle84Ua2MjbScjadpCvFQH5Tutuls2N4/xx"
238 )
239 assert crypto[0]["session-params"] == ""
240
241 def test_parse_description(self):
242 """Parsing <description> to a dict is successful"""
243 description_element = xml_tools.parse(
244 """
245 <description xmlns="urn:xmpp:jingle:apps:rtp:1" media="audio">
246 <payload-type id="96" channels="2" clockrate="48000" maxptime="60" name="opus" ptime="20">
247 <parameter name="sprop-stereo" value="1" />
248 </payload-type>
249 <bandwidth type="AS:40000" />
250 <rtcp-mux />
251 <encryption required="1">
252 <crypto tag="1" crypto-suite="AES_CM_128_HMAC_SHA1_80" key-params="inline:DPSKYRle84Ua2MjbScjadpCvFQH5Tutuls2N4/xx" session-params="" />
253 </encryption>
254 </description>
255 """
256 )
257
258 parsed_data = mapping.parse_description(description_element)
259
260 # Assertions
261 assert parsed_data["payload_types"] == {
262 96: {
263 "channels": "2",
264 "clockrate": "48000",
265 "maxptime": "60",
266 "name": "opus",
267 "ptime": "20",
268 "parameters": {"sprop-stereo": "1"},
269 }
270 }
271 assert parsed_data["bandwidth"] == "AS:40000"
272 assert parsed_data["rtcp-mux"] is True
273 assert parsed_data["encryption_required"] is True
274 assert parsed_data["encryption"] == [
275 {
276 "tag": "1",
277 "crypto-suite": "AES_CM_128_HMAC_SHA1_80",
278 "key-params": "inline:DPSKYRle84Ua2MjbScjadpCvFQH5Tutuls2N4/xx",
279 "session-params": "",
280 }
281 ]
282
283
284 class TestXEP0167:
285 def test_jingle_session_info(self, host, client):
286 """Bridge's call_info method is called with correct parameters."""
287 xep_0167 = XEP_0167(host)
288 session = {"id": "123"}
289 mock_call_info = MagicMock()
290 host.bridge.call_info = mock_call_info
291
292 jingle_elt = xml_tools.parse(
293 """
294 <jingle xmlns='urn:xmpp:jingle:1'
295 action='session-info'
296 initiator='client1@example.org'
297 sid='a73sjjvkla37jfea'>
298 <mute xmlns="urn:xmpp:jingle:apps:rtp:info:1" name="mute_name"/>
299 </jingle>
300 """
301 )
302
303 xep_0167.jingle_session_info(client, "mute", session, "content_name", jingle_elt)
304
305 mock_call_info.assert_called_with(
306 session["id"],
307 "mute",
308 data_format.serialise({"name": "mute_name"}),
309 client.profile,
310 )
311
312 def test_jingle_session_info_invalid_actions(self, host, client):
313 """When receiving invalid actions, no further action is taken."""
314 xep_0167 = XEP_0167(host)
315 session = {"id": "123"}
316 mock_call_info = MagicMock()
317 host.bridge.call_info = mock_call_info
318
319 jingle_elt = xml_tools.parse(
320 """
321 <jingle xmlns='urn:xmpp:jingle:1'
322 action='session-info'
323 initiator='client1@example.org'
324 sid='a73sjjvkla37jfea'>
325 <invalid xmlns="urn:xmpp:jingle:apps:rtp:info:1" name="invalid_name"/>
326 </jingle>
327 """
328 )
329
330 xep_0167.jingle_session_info(
331 client, "invalid", session, "content_name", jingle_elt
332 )
333 mock_call_info.assert_not_called()
334
335 def test_send_info(self, host, client):
336 """A jingle element with the correct info is created and sent."""
337 xep_0167 = XEP_0167(host)
338 session_id = "123"
339 extra = {"name": "test"}
340
341 iq_elt = xml_tools.parse(
342 """
343 <iq from='client1@example.org'
344 id='yh3gr714'
345 to='client2@example.net'
346 type='set'>
347 <jingle xmlns='urn:xmpp:jingle:1'
348 action='session-info'
349 initiator='client1@example.org'
350 sid='a73sjjvkla37jfea'>
351 <active xmlns='urn:xmpp:jingle:apps:rtp:info:1'/>
352 </jingle>
353 </iq>
354 """
355 )
356 jingle_elt = iq_elt.firstChildElement()
357 mock_send = MagicMock()
358 iq_elt.send = mock_send
359
360 with patch.object(
361 xep_0167._j, "build_session_info", return_value=(iq_elt, jingle_elt)
362 ):
363 xep_0167.send_info(client, session_id, "mute", extra)
364
365 info_elt = jingle_elt.firstChildElement()
366 assert info_elt.name == "active"
367 assert info_elt.uri == NS_JINGLE_RTP_INFO
368 mock_send.assert_called()
369
370 def test_send_info_invalid_actions(self, host, client):
371 """When trying to send invalid actions, an error is raised."""
372 xep_0167 = XEP_0167(host)
373 session_id = "123"
374 extra = {"name": "test"}
375
376 with raises(ValueError, match="Unkown info type 'invalid_action'"):
377 xep_0167.send_info(client, session_id, "invalid_action", extra)