Line data Source code
1 : /*
2 : * Copyright (C) 2021-2024 Savoir-faire Linux Inc.
3 : *
4 : * Author: Mohamed Chibani <mohamed.chibani@savoirfairelinux.com>
5 : *
6 : * This program is free software; you can redistribute it and/or modify
7 : * it under the terms of the GNU 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 General Public License for more details.
15 : *
16 : * You should have received a copy of the GNU General Public License
17 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 : */
19 :
20 : #include <cppunit/TestAssert.h>
21 : #include <cppunit/TestFixture.h>
22 : #include <cppunit/extensions/HelperMacros.h>
23 :
24 : #include <condition_variable>
25 : #include <string>
26 :
27 : #include "callmanager_interface.h"
28 : #include "manager.h"
29 : #include "sip/sipaccount.h"
30 : #include "../../test_runner.h"
31 : #include "jami.h"
32 : #include "jami/media_const.h"
33 : #include "call_const.h"
34 : #include "account_const.h"
35 : #include "sip/sipcall.h"
36 : #include "sip/sdp.h"
37 : using namespace libjami::Account;
38 : using namespace libjami::Call;
39 :
40 : namespace jami {
41 : namespace test {
42 :
43 : struct CallData
44 : {
45 : struct Signal
46 : {
47 12 : Signal(const std::string& name, const std::string& event = {})
48 12 : : name_(std::move(name))
49 12 : , event_(std::move(event)) {};
50 :
51 : std::string name_ {};
52 : std::string event_ {};
53 : };
54 :
55 : std::string accountId_ {};
56 : uint16_t listeningPort_ {0};
57 : std::string userName_ {};
58 : std::string alias_ {};
59 : std::string callId_ {};
60 : std::vector<Signal> signals_;
61 : std::condition_variable cv_ {};
62 : std::mutex mtx_;
63 : };
64 :
65 : /**
66 : * Call tests for SIP accounts.
67 : */
68 : class SipSrtpTest : public CppUnit::TestFixture
69 : {
70 : public:
71 1 : SipSrtpTest()
72 1 : {
73 : // Init daemon
74 1 : libjami::init(libjami::InitFlag(libjami::LIBJAMI_FLAG_DEBUG | libjami::LIBJAMI_FLAG_CONSOLE_LOG));
75 1 : if (not Manager::instance().initialized)
76 1 : CPPUNIT_ASSERT(libjami::start("dring-sample.yml"));
77 1 : }
78 2 : ~SipSrtpTest() { libjami::fini(); }
79 :
80 2 : static std::string name() { return "SipSrtpTest"; }
81 : void setUp();
82 : void tearDown();
83 :
84 : private:
85 : // Test cases.
86 : void audio_video_srtp_enabled_test();
87 :
88 2 : CPPUNIT_TEST_SUITE(SipSrtpTest);
89 1 : CPPUNIT_TEST(audio_video_srtp_enabled_test);
90 :
91 4 : CPPUNIT_TEST_SUITE_END();
92 :
93 : // Event/Signal handlers
94 : static void onCallStateChange(const std::string& accountId,
95 : const std::string& callId,
96 : const std::string& state,
97 : CallData& callData);
98 : static void onIncomingCallWithMedia(const std::string& accountId,
99 : const std::string& callId,
100 : const std::vector<libjami::MediaMap> mediaList,
101 : CallData& callData);
102 : static void onMediaNegotiationStatus(const std::string& callId,
103 : const std::string& event,
104 : CallData& callData);
105 :
106 : // Helpers
107 : void audio_video_call(std::vector<MediaAttribute> offer,
108 : std::vector<MediaAttribute> answer,
109 : bool validateMedia = true);
110 : static void configureTest(CallData& bob, CallData& alice);
111 : static std::string getUserAlias(const std::string& callId);
112 : // Wait for a signal from the callbacks. Some signals also report the event that
113 : // triggered the signal a like the StateChange signal.
114 : static bool waitForSignal(CallData& callData,
115 : const std::string& signal,
116 : const std::string& expectedEvent = {});
117 :
118 : private:
119 : CallData aliceData_;
120 : CallData bobData_;
121 : };
122 :
123 : CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(SipSrtpTest, SipSrtpTest::name());
124 :
125 : void
126 1 : SipSrtpTest::setUp()
127 : {
128 1 : aliceData_.listeningPort_ = 5080;
129 2 : std::map<std::string, std::string> details = libjami::getAccountTemplate("SIP");
130 1 : details[ConfProperties::TYPE] = "SIP";
131 1 : details[ConfProperties::DISPLAYNAME] = "ALICE";
132 1 : details[ConfProperties::ALIAS] = "ALICE";
133 1 : details[ConfProperties::LOCAL_PORT] = std::to_string(aliceData_.listeningPort_);
134 1 : details[ConfProperties::UPNP_ENABLED] = "false";
135 1 : details[ConfProperties::SRTP::KEY_EXCHANGE] = "sdes";
136 1 : aliceData_.accountId_ = Manager::instance().addAccount(details);
137 :
138 1 : bobData_.listeningPort_ = 5082;
139 1 : details = libjami::getAccountTemplate("SIP");
140 1 : details[ConfProperties::TYPE] = "SIP";
141 1 : details[ConfProperties::DISPLAYNAME] = "BOB";
142 1 : details[ConfProperties::ALIAS] = "BOB";
143 1 : details[ConfProperties::LOCAL_PORT] = std::to_string(bobData_.listeningPort_);
144 1 : details[ConfProperties::UPNP_ENABLED] = "false";
145 1 : details[ConfProperties::SRTP::KEY_EXCHANGE] = "sdes";
146 1 : bobData_.accountId_ = Manager::instance().addAccount(details);
147 :
148 1 : JAMI_INFO("Initialize accounts ...");
149 1 : auto aliceAccount = Manager::instance().getAccount<SIPAccount>(aliceData_.accountId_);
150 1 : auto bobAccount = Manager::instance().getAccount<SIPAccount>(bobData_.accountId_);
151 1 : }
152 :
153 : void
154 1 : SipSrtpTest::tearDown()
155 : {
156 1 : JAMI_INFO("Remove created accounts...");
157 :
158 1 : std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> confHandlers;
159 1 : std::mutex mtx;
160 1 : std::unique_lock lk {mtx};
161 1 : std::condition_variable cv;
162 1 : auto currentAccSize = Manager::instance().getAccountList().size();
163 1 : std::atomic_bool accountsRemoved {false};
164 1 : confHandlers.insert(
165 2 : libjami::exportable_callback<libjami::ConfigurationSignal::AccountsChanged>([&]() {
166 2 : if (Manager::instance().getAccountList().size() <= currentAccSize - 2) {
167 1 : accountsRemoved = true;
168 1 : cv.notify_one();
169 : }
170 2 : }));
171 1 : libjami::registerSignalHandlers(confHandlers);
172 :
173 1 : Manager::instance().removeAccount(aliceData_.accountId_, true);
174 1 : Manager::instance().removeAccount(bobData_.accountId_, true);
175 : // Because cppunit is not linked with dbus, just poll if removed
176 2 : CPPUNIT_ASSERT(
177 : cv.wait_for(lk, std::chrono::seconds(30), [&] { return accountsRemoved.load(); }));
178 :
179 1 : libjami::unregisterSignalHandlers();
180 1 : }
181 :
182 : std::string
183 14 : SipSrtpTest::getUserAlias(const std::string& callId)
184 : {
185 14 : auto call = Manager::instance().getCallFromCallID(callId);
186 :
187 14 : if (not call) {
188 2 : JAMI_WARN("Call with ID [%s] does not exist anymore!", callId.c_str());
189 2 : return {};
190 : }
191 :
192 12 : auto const& account = call->getAccount().lock();
193 12 : if (not account) {
194 0 : return {};
195 : }
196 :
197 12 : return account->getAccountDetails()[ConfProperties::ALIAS];
198 14 : }
199 :
200 : void
201 1 : SipSrtpTest::onIncomingCallWithMedia(const std::string& accountId,
202 : const std::string& callId,
203 : const std::vector<libjami::MediaMap> mediaList,
204 : CallData& callData)
205 : {
206 1 : CPPUNIT_ASSERT_EQUAL(callData.accountId_, accountId);
207 :
208 1 : JAMI_INFO("Signal [%s] - user [%s] - call [%s] - media count [%lu]",
209 : libjami::CallSignal::IncomingCallWithMedia::name,
210 : callData.alias_.c_str(),
211 : callId.c_str(),
212 : mediaList.size());
213 :
214 : // NOTE.
215 : // We shouldn't access shared_ptr<Call> as this event is supposed to mimic
216 : // the client, and the client have no access to this type. But here, we only
217 : // needed to check if the call exists. This is the most straightforward and
218 : // reliable way to do it until we add a new API (like hasCall(id)).
219 1 : if (not Manager::instance().getCallFromCallID(callId)) {
220 0 : JAMI_WARN("Call with ID [%s] does not exist!", callId.c_str());
221 0 : callData.callId_ = {};
222 0 : return;
223 : }
224 :
225 1 : std::unique_lock lock {callData.mtx_};
226 1 : callData.callId_ = callId;
227 1 : callData.signals_.emplace_back(CallData::Signal(libjami::CallSignal::IncomingCallWithMedia::name));
228 :
229 1 : callData.cv_.notify_one();
230 1 : }
231 :
232 : void
233 9 : SipSrtpTest::onCallStateChange(const std::string&,
234 : const std::string& callId,
235 : const std::string& state,
236 : CallData& callData)
237 : {
238 9 : auto call = Manager::instance().getCallFromCallID(callId);
239 9 : if (not call) {
240 0 : JAMI_WARN("Call with ID [%s] does not exist anymore!", callId.c_str());
241 0 : return;
242 : }
243 :
244 9 : auto account = call->getAccount().lock();
245 9 : if (not account) {
246 0 : JAMI_WARN("Account owning the call [%s] does not exist!", callId.c_str());
247 0 : return;
248 : }
249 :
250 9 : JAMI_INFO("Signal [%s] - user [%s] - call [%s] - state [%s]",
251 : libjami::CallSignal::StateChange::name,
252 : callData.alias_.c_str(),
253 : callId.c_str(),
254 : state.c_str());
255 :
256 9 : if (account->getAccountID() != callData.accountId_)
257 0 : return;
258 :
259 : {
260 9 : std::unique_lock lock {callData.mtx_};
261 9 : callData.signals_.emplace_back(
262 18 : CallData::Signal(libjami::CallSignal::StateChange::name, state));
263 9 : }
264 : // NOTE. Only states that we are interested on will notify the CV. If this
265 : // unit test is modified to process other states, they must be added here.
266 9 : if (state == "CURRENT" or state == "OVER" or state == "HUNGUP" or state == "RINGING") {
267 5 : callData.cv_.notify_one();
268 : }
269 9 : }
270 :
271 : void
272 2 : SipSrtpTest::onMediaNegotiationStatus(const std::string& callId,
273 : const std::string& event,
274 : CallData& callData)
275 : {
276 2 : auto call = Manager::instance().getCallFromCallID(callId);
277 2 : if (not call) {
278 0 : JAMI_WARN("Call with ID [%s] does not exist!", callId.c_str());
279 0 : return;
280 : }
281 :
282 2 : auto account = call->getAccount().lock();
283 2 : if (not account) {
284 0 : JAMI_WARN("Account owning the call [%s] does not exist!", callId.c_str());
285 0 : return;
286 : }
287 :
288 2 : JAMI_INFO("Signal [%s] - user [%s] - call [%s] - state [%s]",
289 : libjami::CallSignal::MediaNegotiationStatus::name,
290 : account->getAccountDetails()[ConfProperties::ALIAS].c_str(),
291 : call->getCallId().c_str(),
292 : event.c_str());
293 :
294 2 : if (account->getAccountID() != callData.accountId_)
295 0 : return;
296 :
297 : {
298 2 : std::unique_lock lock {callData.mtx_};
299 2 : callData.signals_.emplace_back(
300 4 : CallData::Signal(libjami::CallSignal::MediaNegotiationStatus::name, event));
301 2 : }
302 :
303 2 : callData.cv_.notify_one();
304 2 : }
305 :
306 : bool
307 6 : SipSrtpTest::waitForSignal(CallData& callData,
308 : const std::string& expectedSignal,
309 : const std::string& expectedEvent)
310 : {
311 6 : const std::chrono::seconds TIME_OUT {30};
312 6 : std::unique_lock lock {callData.mtx_};
313 :
314 : // Combined signal + event (if any).
315 6 : std::string sigEvent(expectedSignal);
316 6 : if (not expectedEvent.empty())
317 5 : sigEvent += "::" + expectedEvent;
318 :
319 6 : JAMI_INFO("[%s] is waiting for [%s] signal/event", callData.alias_.c_str(), sigEvent.c_str());
320 :
321 6 : auto res = callData.cv_.wait_for(lock, TIME_OUT, [&] {
322 : // Search for the expected signal in list of received signals.
323 9 : bool pred = false;
324 32 : for (auto it = callData.signals_.begin(); it != callData.signals_.end(); it++) {
325 : // The predicate is true if the signal names match, and if the
326 : // expectedEvent is not empty, the events must also match.
327 29 : if (it->name_ == expectedSignal
328 29 : and (expectedEvent.empty() or it->event_ == expectedEvent)) {
329 6 : pred = true;
330 : // Done with this signal.
331 6 : callData.signals_.erase(it);
332 6 : break;
333 : }
334 : }
335 :
336 9 : return pred;
337 : });
338 :
339 6 : if (not res) {
340 0 : JAMI_ERR("[%s] waiting for signal/event [%s] timed-out!",
341 : callData.alias_.c_str(),
342 : sigEvent.c_str());
343 :
344 0 : JAMI_INFO("[%s] currently has the following signals:", callData.alias_.c_str());
345 :
346 0 : for (auto const& sig : callData.signals_) {
347 0 : JAMI_INFO() << "Signal [" << sig.name_
348 0 : << (sig.event_.empty() ? "" : ("::" + sig.event_)) << "]";
349 : }
350 : }
351 :
352 6 : return res;
353 6 : }
354 :
355 : void
356 1 : SipSrtpTest::configureTest(CallData& aliceData, CallData& bobData)
357 : {
358 : {
359 1 : CPPUNIT_ASSERT(not aliceData.accountId_.empty());
360 1 : auto const& account = Manager::instance().getAccount<SIPAccount>(aliceData.accountId_);
361 1 : aliceData.userName_ = account->getAccountDetails()[ConfProperties::USERNAME];
362 1 : aliceData.alias_ = account->getAccountDetails()[ConfProperties::ALIAS];
363 1 : account->setLocalPort(aliceData.listeningPort_);
364 1 : }
365 :
366 : {
367 1 : CPPUNIT_ASSERT(not bobData.accountId_.empty());
368 1 : auto const& account = Manager::instance().getAccount<SIPAccount>(bobData.accountId_);
369 1 : bobData.userName_ = account->getAccountDetails()[ConfProperties::USERNAME];
370 1 : bobData.alias_ = account->getAccountDetails()[ConfProperties::ALIAS];
371 1 : account->setLocalPort(bobData.listeningPort_);
372 1 : }
373 :
374 1 : std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> signalHandlers;
375 :
376 : // Insert needed signal handlers.
377 1 : signalHandlers.insert(libjami::exportable_callback<libjami::CallSignal::IncomingCallWithMedia>(
378 1 : [&](const std::string& accountId,
379 : const std::string& callId,
380 : const std::string&,
381 : const std::vector<libjami::MediaMap> mediaList) {
382 1 : auto user = getUserAlias(callId);
383 1 : if (not user.empty())
384 1 : onIncomingCallWithMedia(accountId,
385 : callId,
386 : mediaList,
387 1 : user == aliceData.alias_ ? aliceData : bobData);
388 1 : }));
389 :
390 1 : signalHandlers.insert(
391 2 : libjami::exportable_callback<libjami::CallSignal::StateChange>([&](const std::string& accountId,
392 : const std::string& callId,
393 : const std::string& state,
394 : signed) {
395 11 : auto user = getUserAlias(callId);
396 11 : if (not user.empty())
397 9 : onCallStateChange(accountId,
398 : callId,
399 : state,
400 9 : user == aliceData.alias_ ? aliceData : bobData);
401 11 : }));
402 :
403 1 : signalHandlers.insert(libjami::exportable_callback<libjami::CallSignal::MediaNegotiationStatus>(
404 2 : [&](const std::string& callId,
405 : const std::string& event,
406 : const std::vector<std::map<std::string, std::string>>& /* mediaList */) {
407 2 : auto user = getUserAlias(callId);
408 2 : if (not user.empty())
409 2 : onMediaNegotiationStatus(callId,
410 : event,
411 2 : user == aliceData.alias_ ? aliceData : bobData);
412 2 : }));
413 :
414 1 : libjami::registerSignalHandlers(signalHandlers);
415 1 : }
416 :
417 : void
418 1 : SipSrtpTest::audio_video_call(std::vector<MediaAttribute> offer,
419 : std::vector<MediaAttribute> answer,
420 : bool validateMedia)
421 : {
422 1 : JAMI_INFO("=== Begin test %s ===", __FUNCTION__);
423 :
424 1 : configureTest(aliceData_, bobData_);
425 :
426 1 : JAMI_INFO("=== Start a call and validate ===");
427 :
428 1 : std::string bobUri = "127.0.0.1:" + std::to_string(bobData_.listeningPort_);
429 :
430 2 : aliceData_.callId_ = libjami::placeCallWithMedia(aliceData_.accountId_,
431 : bobUri,
432 2 : MediaAttribute::mediaAttributesToMediaMaps(
433 1 : offer));
434 :
435 1 : CPPUNIT_ASSERT(not aliceData_.callId_.empty());
436 :
437 1 : JAMI_INFO("ALICE [%s] started a call with BOB [%s] and wait for answer",
438 : aliceData_.accountId_.c_str(),
439 : bobData_.accountId_.c_str());
440 :
441 : // Give it some time to ring
442 1 : std::this_thread::sleep_for(std::chrono::seconds(2));
443 :
444 : // Wait for call to be processed.
445 1 : CPPUNIT_ASSERT(
446 : waitForSignal(aliceData_, libjami::CallSignal::StateChange::name, StateEvent::RINGING));
447 :
448 : // Wait for incoming call signal.
449 1 : CPPUNIT_ASSERT(waitForSignal(bobData_, libjami::CallSignal::IncomingCallWithMedia::name));
450 :
451 : // Answer the call.
452 1 : libjami::acceptWithMedia(bobData_.accountId_,
453 1 : bobData_.callId_,
454 2 : MediaAttribute::mediaAttributesToMediaMaps(answer));
455 :
456 : // Wait for media negotiation complete signal.
457 1 : CPPUNIT_ASSERT(waitForSignal(bobData_,
458 : libjami::CallSignal::MediaNegotiationStatus::name,
459 : libjami::Media::MediaNegotiationStatusEvents::NEGOTIATION_SUCCESS));
460 :
461 : // Wait for the StateChange signal.
462 1 : CPPUNIT_ASSERT(
463 : waitForSignal(bobData_, libjami::CallSignal::StateChange::name, StateEvent::CURRENT));
464 :
465 1 : JAMI_INFO("BOB answered the call [%s]", bobData_.callId_.c_str());
466 :
467 : // Wait for media negotiation complete signal.
468 1 : CPPUNIT_ASSERT(waitForSignal(aliceData_,
469 : libjami::CallSignal::MediaNegotiationStatus::name,
470 : libjami::Media::MediaNegotiationStatusEvents::NEGOTIATION_SUCCESS));
471 :
472 : // Validate Alice's media
473 1 : if (validateMedia) {
474 1 : auto call = Manager::instance().getCallFromCallID(aliceData_.callId_);
475 1 : auto activeMediaList = call->getMediaAttributeList();
476 1 : CPPUNIT_ASSERT_EQUAL(offer.size(), activeMediaList.size());
477 : // Audio
478 1 : CPPUNIT_ASSERT_EQUAL(MediaType::MEDIA_AUDIO, activeMediaList[0].type_);
479 1 : CPPUNIT_ASSERT_EQUAL(offer[0].enabled_, activeMediaList[0].enabled_);
480 :
481 : // Video
482 1 : if (offer.size() > 1) {
483 1 : CPPUNIT_ASSERT_EQUAL(MediaType::MEDIA_VIDEO, activeMediaList[1].type_);
484 1 : CPPUNIT_ASSERT_EQUAL(offer[1].enabled_, activeMediaList[1].enabled_);
485 : }
486 1 : }
487 :
488 : // Validate Bob's media
489 1 : if (validateMedia) {
490 1 : auto call = Manager::instance().getCallFromCallID(bobData_.callId_);
491 1 : auto activeMediaList = call->getMediaAttributeList();
492 1 : CPPUNIT_ASSERT_EQUAL(answer.size(), activeMediaList.size());
493 : // Audio
494 1 : CPPUNIT_ASSERT_EQUAL(MediaType::MEDIA_AUDIO, activeMediaList[0].type_);
495 1 : CPPUNIT_ASSERT_EQUAL(answer[0].enabled_, activeMediaList[0].enabled_);
496 :
497 : // Video
498 1 : if (offer.size() > 1) {
499 1 : CPPUNIT_ASSERT_EQUAL(MediaType::MEDIA_VIDEO, activeMediaList[1].type_);
500 1 : CPPUNIT_ASSERT_EQUAL(answer[1].enabled_, activeMediaList[1].enabled_);
501 : }
502 1 : }
503 :
504 : // Give some time to media to start and flow
505 1 : std::this_thread::sleep_for(std::chrono::seconds(3));
506 :
507 : // Bob hang-up.
508 1 : JAMI_INFO("Hang up BOB's call and wait for ALICE to hang up");
509 1 : libjami::hangUp(bobData_.accountId_, bobData_.callId_);
510 :
511 1 : CPPUNIT_ASSERT(
512 : waitForSignal(aliceData_, libjami::CallSignal::StateChange::name, StateEvent::HUNGUP));
513 :
514 1 : JAMI_INFO("Call terminated on both sides");
515 1 : }
516 :
517 : void
518 1 : SipSrtpTest::audio_video_srtp_enabled_test()
519 : {
520 : // Test with video enabled on Alice's side and disabled
521 : // on Bob's side.
522 :
523 1 : auto const aliceAcc = Manager::instance().getAccount<SIPAccount>(aliceData_.accountId_);
524 1 : CPPUNIT_ASSERT(aliceAcc->isSrtpEnabled());
525 :
526 1 : auto const bobAcc = Manager::instance().getAccount<SIPAccount>(bobData_.accountId_);
527 1 : CPPUNIT_ASSERT(bobAcc->isSrtpEnabled());
528 :
529 1 : std::vector<MediaAttribute> offer;
530 1 : std::vector<MediaAttribute> answer;
531 :
532 1 : MediaAttribute audio(MediaType::MEDIA_AUDIO);
533 1 : MediaAttribute video(MediaType::MEDIA_VIDEO);
534 :
535 : // Configure Alice
536 1 : audio.enabled_ = true;
537 1 : audio.label_ = "audio_0";
538 1 : offer.emplace_back(audio);
539 :
540 1 : video.enabled_ = true;
541 1 : video.label_ = "video_0";
542 1 : aliceAcc->enableVideo(true);
543 1 : offer.emplace_back(video);
544 :
545 : // Configure Bob
546 1 : audio.enabled_ = true;
547 1 : audio.label_ = "audio_0";
548 1 : answer.emplace_back(audio);
549 :
550 1 : video.enabled_ = false;
551 1 : video.label_ = "video_0";
552 1 : bobAcc->enableVideo(false);
553 1 : answer.emplace_back(video);
554 :
555 : // Run the scenario
556 1 : audio_video_call(offer, answer);
557 1 : }
558 :
559 : } // namespace test
560 : } // namespace jami
561 :
562 1 : RING_TEST_RUNNER(jami::test::SipSrtpTest::name())
|