Mercurial > libervia-backend
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) |