From 38981578d5b391079919d1a65bee72825bc2dc71 Mon Sep 17 00:00:00 2001
From: Mohamed Chibani <mohamed.chibani@savoirfairelinux.com>
Date: Thu, 18 Nov 2021 09:31:49 -0500
Subject: [PATCH] auto-answer - add unit test

Add a media negotiation unit test for auto-answer mode

Gitlab: #645

Change-Id: I37f768ce48e078fbd95a2c9b28997877a4dc468d
---
 src/client/callmanager.cpp                    |    6 +-
 src/sip/sipcall.cpp                           |   34 +-
 test/test_runner.h                            |   51 +-
 test/unitTest/Makefile.am                     |    2 +
 .../media_negotiation/auto_answer.cpp         | 1030 +++++++++++++++++
 .../media_negotiation/media_negotiation.cpp   |   10 +-
 6 files changed, 1103 insertions(+), 30 deletions(-)
 create mode 100644 test/unitTest/media_negotiation/auto_answer.cpp

diff --git a/src/client/callmanager.cpp b/src/client/callmanager.cpp
index 6c19f172af..847e3bc040 100644
--- a/src/client/callmanager.cpp
+++ b/src/client/callmanager.cpp
@@ -200,7 +200,7 @@ createConfFromParticipantList(const std::string& accountId,
 void
 setConferenceLayout(const std::string& accountId, const std::string& confId, uint32_t layout)
 {
-    if (const auto account = jami::Manager::instance().getAccount(accountId))
+    if (const auto account = jami::Manager::instance().getAccount(accountId)) {
         if (auto conf = account->getConference(confId)) {
             conf->setLayout(layout);
         } else if (auto call = account->getCall(confId)) {
@@ -208,6 +208,7 @@ setConferenceLayout(const std::string& accountId, const std::string& confId, uin
             root["layout"] = layout;
             call->sendConfOrder(root);
         }
+    }
 }
 
 void
@@ -215,7 +216,7 @@ setActiveParticipant(const std::string& accountId,
                      const std::string& confId,
                      const std::string& participant)
 {
-    if (const auto account = jami::Manager::instance().getAccount(accountId))
+    if (const auto account = jami::Manager::instance().getAccount(accountId)) {
         if (auto conf = account->getConference(confId)) {
             conf->setActiveParticipant(participant);
         } else if (auto call = account->getCall(confId)) {
@@ -223,6 +224,7 @@ setActiveParticipant(const std::string& accountId,
             root["activeParticipant"] = participant;
             call->sendConfOrder(root);
         }
+    }
 }
 
 bool
diff --git a/src/sip/sipcall.cpp b/src/sip/sipcall.cpp
index fff0fc510f..b47dc3f74c 100644
--- a/src/sip/sipcall.cpp
+++ b/src/sip/sipcall.cpp
@@ -2524,19 +2524,29 @@ SIPCall::handleMediaChangeRequest(const std::vector<DRing::MediaMap>& remoteMedi
         return;
     }
 
-    // If the offered media differ from the current local media, the
-    // request is reported to the client to be processed. Otherwise,
-    // it will be processed using the current local media.
-
-    if (checkMediaChangeRequest(remoteMediaList)) {
-        // Report the media change request.
-        emitSignal<DRing::CallSignal::MediaChangeRequested>(getAccountId(),
-                                                            getCallId(),
-                                                            remoteMediaList);
-    } else {
-        auto localMediaList = MediaAttribute::mediaAttributesToMediaMaps(getMediaAttributeList());
-        answerMediaChangeRequest(localMediaList);
+    // If the offered media does not differ from the current local media, the
+    // request is answered using the current local media.
+    if (not checkMediaChangeRequest(remoteMediaList)) {
+        answerMediaChangeRequest(
+            MediaAttribute::mediaAttributesToMediaMaps(getMediaAttributeList()));
+        return;
+    }
+
+    if (account->isAutoAnswerEnabled()) {
+        // NOTE:
+        // Since the auto-answer is enabled in the account, media change requests
+        // are automatically accepted too. This also means that if the original
+        // call was an audio-only call, and the remote added the video, the local
+        // camera will be enabled, unless the video is disabled in the account
+        // settings.
+        answerMediaChangeRequest(remoteMediaList);
+        return;
     }
+
+    // Report the media change request.
+    emitSignal<DRing::CallSignal::MediaChangeRequested>(getAccountId(),
+                                                        getCallId(),
+                                                        remoteMediaList);
 }
 
 pj_status_t
diff --git a/test/test_runner.h b/test/test_runner.h
index 0762b5764c..7b5584c196 100644
--- a/test/test_runner.h
+++ b/test/test_runner.h
@@ -5,15 +5,42 @@
 #include <cppunit/CompilerOutputter.h>
 
 #define RING_TEST_RUNNER(suite_name) \
-int main() \
-{ \
-    CppUnit::TestFactoryRegistry &registry = CppUnit::TestFactoryRegistry::getRegistry(suite_name); \
-    CppUnit::Test *suite = registry.makeTest(); \
-    if(suite->countTestCases() == 0) { \
-        std::cout << "No test cases specified for suite \"" << suite_name << "\"\n"; \
-        return 1; \
-    } \
-    CppUnit::TextUi::TestRunner runner; \
-    runner.addTest(suite); \
-    return runner.run() ? 0 : 1; \
-}
+    int main() \
+    { \
+        CppUnit::TestFactoryRegistry& registry = CppUnit::TestFactoryRegistry::getRegistry( \
+            suite_name); \
+        CppUnit::Test* suite = registry.makeTest(); \
+        if (suite->countTestCases() == 0) { \
+            std::cout << "No test cases specified for suite \"" << suite_name << "\"\n"; \
+            return 1; \
+        } \
+        CppUnit::TextUi::TestRunner runner; \
+        runner.addTest(suite); \
+        return runner.run() ? 0 : 1; \
+    }
+
+// This version of the test runner is similar to RING_TEST_RUNNER but
+// can take multiple unit tests.
+// It's practical to run a test for diffrent configs, for instance when
+// running the same test for both Jami and SIP accounts.
+
+// The test will abort if a test fails.
+#define JAMI_TEST_RUNNER(...) \
+    int main() \
+    { \
+        std::vector<std::string> suite_names {__VA_ARGS__}; \
+        for (const std::string& name : suite_names) { \
+            CppUnit::TestFactoryRegistry& registry = CppUnit::TestFactoryRegistry::getRegistry( \
+                name); \
+            CppUnit::Test* suite = registry.makeTest(); \
+            if (suite->countTestCases() == 0) { \
+                std::cout << "No test cases specified for suite \"" << name << "\"\n"; \
+                continue; \
+            } \
+            CppUnit::TextUi::TestRunner runner; \
+            runner.addTest(suite); \
+            if (not runner.run()) \
+                return 1; \
+        } \
+        return 0; \
+    }
diff --git a/test/unitTest/Makefile.am b/test/unitTest/Makefile.am
index ad2bf69252..7cbd07727c 100644
--- a/test/unitTest/Makefile.am
+++ b/test/unitTest/Makefile.am
@@ -157,6 +157,8 @@ check_PROGRAMS += ut_media_negotiation
 ut_media_negotiation_SOURCES = media_negotiation/media_negotiation.cpp common.cpp
 check_PROGRAMS += ut_hold_resume
 ut_hold_resume_SOURCES = media_negotiation/hold_resume.cpp common.cpp
+check_PROGRAMS += ut_auto_answer
+ut_auto_answer_SOURCES = media_negotiation/auto_answer.cpp common.cpp
 
 #
 # compability
diff --git a/test/unitTest/media_negotiation/auto_answer.cpp b/test/unitTest/media_negotiation/auto_answer.cpp
new file mode 100644
index 0000000000..33e9331438
--- /dev/null
+++ b/test/unitTest/media_negotiation/auto_answer.cpp
@@ -0,0 +1,1030 @@
+/*
+ *  Copyright (C) 2021 Savoir-faire Linux Inc.
+ *
+ *  Author: Mohamed Chibani <mohamed.chibani@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU 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 General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <cppunit/TestAssert.h>
+#include <cppunit/TestFixture.h>
+#include <cppunit/extensions/HelperMacros.h>
+
+#include <condition_variable>
+#include <string>
+
+#include "../../test_runner.h"
+
+#include "manager.h"
+#include "jamidht/connectionmanager.h"
+#include "account.h"
+#include "sip/sipaccount.h"
+#include "jami.h"
+#include "jami/media_const.h"
+#include "call_const.h"
+#include "account_const.h"
+#include "sip/sipcall.h"
+#include "sip/sdp.h"
+
+#include "common.h"
+
+using namespace DRing::Account;
+using namespace DRing::Call;
+
+namespace jami {
+namespace test {
+
+struct TestScenario
+{
+    TestScenario(const std::vector<MediaAttribute>& offer,
+                 const std::vector<MediaAttribute>& answer,
+                 const std::vector<MediaAttribute>& offerUpdate,
+                 const std::vector<MediaAttribute>& answerUpdate)
+        : offer_(std::move(offer))
+        , answer_(std::move(answer))
+        , offerUpdate_(std::move(offerUpdate))
+        , answerUpdate_(std::move(answerUpdate))
+    {}
+
+    TestScenario() {};
+
+    std::vector<MediaAttribute> offer_;
+    std::vector<MediaAttribute> answer_;
+    std::vector<MediaAttribute> offerUpdate_;
+    std::vector<MediaAttribute> answerUpdate_;
+    // Determine if we should expect the MediaNegotiationStatus signal.
+    bool expectMediaRenegotiation_ {false};
+};
+
+struct CallData
+{
+    struct Signal
+    {
+        Signal(const std::string& name, const std::string& event = {})
+            : name_(std::move(name))
+            , event_(std::move(event)) {};
+
+        std::string name_ {};
+        std::string event_ {};
+    };
+
+    std::string accountId_ {};
+    std::string userName_ {};
+    std::string alias_ {};
+    std::string callId_ {};
+    uint16_t listeningPort_ {0};
+    std::string toUri_ {};
+    std::vector<Signal> signals_;
+    std::condition_variable cv_ {};
+    std::mutex mtx_;
+};
+
+class AutoAnswerMediaNegoTest
+{
+public:
+    AutoAnswerMediaNegoTest()
+    {
+        // Init daemon
+        DRing::init(DRing::InitFlag(DRing::DRING_FLAG_DEBUG | DRing::DRING_FLAG_CONSOLE_LOG));
+        if (not Manager::instance().initialized)
+            CPPUNIT_ASSERT(DRing::start("jami-sample.yml"));
+    }
+    ~AutoAnswerMediaNegoTest() { DRing::fini(); }
+
+protected:
+    // Test cases.
+    void audio_and_video_then_caller_mute_video();
+    void audio_only_then_caller_add_video();
+    void audio_and_video_then_caller_mute_audio();
+    void audio_and_video_then_change_video_source();
+
+    // Event/Signal handlers
+    static void onCallStateChange(const std::string& accountId,
+                                  const std::string& callId,
+                                  const std::string& state,
+                                  CallData& callData);
+    static void onIncomingCallWithMedia(const std::string& accountId,
+                                        const std::string& callId,
+                                        const std::vector<DRing::MediaMap> mediaList,
+                                        CallData& callData);
+    static void onMediaChangeRequested(const std::string& accountId,
+                                       const std::string& callId,
+                                       const std::vector<DRing::MediaMap> mediaList,
+                                       CallData& callData);
+    static void onVideoMuted(const std::string& callId, bool muted, CallData& callData);
+    static void onMediaNegotiationStatus(const std::string& callId,
+                                         const std::string& event,
+                                         CallData& callData);
+
+    // Helpers
+    void configureScenario();
+    void testWithScenario(CallData& aliceData, CallData& bobData, const TestScenario& scenario);
+    static std::string getUserAlias(const std::string& callId);
+    // Infer media direction of an offer.
+    static uint8_t directionToBitset(MediaDirection direction, bool isLocal);
+    static MediaDirection bitsetToDirection(uint8_t val);
+    static MediaDirection inferInitialDirection(const MediaAttribute& offer);
+    // Infer media direction of an answer.
+    static MediaDirection inferNegotiatedDirection(MediaDirection local, MediaDirection answer);
+    // Wait for a signal from the callbacks. Some signals also report the event that
+    // triggered the signal like the StateChange signal.
+    static bool validateMuteState(std::vector<MediaAttribute> expected,
+                                  std::vector<MediaAttribute> actual);
+    static bool validateMediaDirection(std::vector<MediaDescription> descrList,
+                                       std::vector<MediaAttribute> listInOffer,
+                                       std::vector<MediaAttribute> listInAnswer);
+    static bool waitForSignal(CallData& callData,
+                              const std::string& signal,
+                              const std::string& expectedEvent = {});
+
+    bool isSipAccount_ {false};
+    CallData aliceData_;
+    CallData bobData_;
+};
+
+class AutoAnswerMediaNegoTestSip : public AutoAnswerMediaNegoTest, public CppUnit::TestFixture
+{
+public:
+    AutoAnswerMediaNegoTestSip() { isSipAccount_ = true; };
+
+    ~AutoAnswerMediaNegoTestSip() {};
+
+    static std::string name() { return "AutoAnswerMediaNegoTestSip"; }
+    void setUp() override;
+    void tearDown() override;
+
+private:
+    CPPUNIT_TEST_SUITE(AutoAnswerMediaNegoTestSip);
+    CPPUNIT_TEST(audio_and_video_then_caller_mute_video);
+    CPPUNIT_TEST(audio_only_then_caller_add_video);
+    CPPUNIT_TEST(audio_and_video_then_caller_mute_audio);
+    CPPUNIT_TEST(audio_and_video_then_change_video_source);
+    CPPUNIT_TEST_SUITE_END();
+};
+
+void
+AutoAnswerMediaNegoTestSip::setUp()
+{
+    aliceData_.listeningPort_ = 5080;
+    std::map<std::string, std::string> details = DRing::getAccountTemplate("SIP");
+    details[ConfProperties::TYPE] = "SIP";
+    details[ConfProperties::DISPLAYNAME] = "ALICE";
+    details[ConfProperties::ALIAS] = "ALICE";
+    details[ConfProperties::LOCAL_PORT] = std::to_string(aliceData_.listeningPort_);
+    details[ConfProperties::UPNP_ENABLED] = "false";
+    aliceData_.accountId_ = Manager::instance().addAccount(details);
+
+    bobData_.listeningPort_ = 5082;
+    details = DRing::getAccountTemplate("SIP");
+    details[ConfProperties::TYPE] = "SIP";
+    details[ConfProperties::DISPLAYNAME] = "BOB";
+    details[ConfProperties::ALIAS] = "BOB";
+    details[ConfProperties::AUTOANSWER] = "true";
+    details[ConfProperties::LOCAL_PORT] = std::to_string(bobData_.listeningPort_);
+    details[ConfProperties::UPNP_ENABLED] = "false";
+    bobData_.accountId_ = Manager::instance().addAccount(details);
+
+    JAMI_INFO("Initialize accounts ...");
+    auto aliceAccount = Manager::instance().getAccount<Account>(aliceData_.accountId_);
+    auto bobAccount = Manager::instance().getAccount<Account>(bobData_.accountId_);
+}
+
+void
+AutoAnswerMediaNegoTestSip::tearDown()
+{
+    JAMI_INFO("Remove created accounts...");
+
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    auto currentAccSize = Manager::instance().getAccountList().size();
+    std::atomic_bool accountsRemoved {false};
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConfigurationSignal::AccountsChanged>([&]() {
+            if (Manager::instance().getAccountList().size() <= currentAccSize - 2) {
+                accountsRemoved = true;
+                cv.notify_one();
+            }
+        }));
+    DRing::registerSignalHandlers(confHandlers);
+
+    Manager::instance().removeAccount(aliceData_.accountId_, true);
+    Manager::instance().removeAccount(bobData_.accountId_, true);
+    CPPUNIT_ASSERT(
+        cv.wait_for(lk, std::chrono::seconds(30), [&] { return accountsRemoved.load(); }));
+
+    DRing::unregisterSignalHandlers();
+}
+
+class AutoAnswerMediaNegoTestJami : public AutoAnswerMediaNegoTest, public CppUnit::TestFixture
+{
+public:
+    AutoAnswerMediaNegoTestJami() { isSipAccount_ = false; };
+
+    ~AutoAnswerMediaNegoTestJami() {};
+
+    static std::string name() { return "AutoAnswerMediaNegoTestJami"; }
+    void setUp() override;
+    void tearDown() override;
+
+private:
+    CPPUNIT_TEST_SUITE(AutoAnswerMediaNegoTestJami);
+    CPPUNIT_TEST(audio_and_video_then_caller_mute_video);
+    CPPUNIT_TEST(audio_only_then_caller_add_video);
+    CPPUNIT_TEST(audio_and_video_then_caller_mute_audio);
+    CPPUNIT_TEST(audio_and_video_then_change_video_source);
+    CPPUNIT_TEST_SUITE_END();
+};
+
+void
+AutoAnswerMediaNegoTestJami::setUp()
+{
+    auto actors = load_actors("actors/alice-bob-no-upnp.yml");
+
+    aliceData_.accountId_ = actors["alice"];
+    bobData_.accountId_ = actors["bob"];
+
+    JAMI_INFO("Initialize account...");
+    auto aliceAccount = Manager::instance().getAccount<Account>(aliceData_.accountId_);
+    auto bobAccount = Manager::instance().getAccount<Account>(bobData_.accountId_);
+    auto details = bobAccount->getAccountDetails();
+    details[ConfProperties::AUTOANSWER] = "true";
+    bobAccount->setAccountDetails(details);
+    wait_for_announcement_of({aliceAccount->getAccountID(), bobAccount->getAccountID()});
+}
+
+void
+AutoAnswerMediaNegoTestJami::tearDown()
+{
+    wait_for_removal_of({aliceData_.accountId_, bobData_.accountId_});
+}
+
+std::string
+AutoAnswerMediaNegoTest::getUserAlias(const std::string& callId)
+{
+    auto call = Manager::instance().getCallFromCallID(callId);
+
+    if (not call) {
+        JAMI_WARN("Call with ID [%s] does not exist anymore!", callId.c_str());
+        return {};
+    }
+
+    auto const& account = call->getAccount().lock();
+    if (not account) {
+        return {};
+    }
+
+    return account->getAccountDetails()[ConfProperties::ALIAS];
+}
+
+MediaDirection
+AutoAnswerMediaNegoTest::inferInitialDirection(const MediaAttribute& mediaAttr)
+{
+    if (not mediaAttr.enabled_)
+        return MediaDirection::INACTIVE;
+
+    if (mediaAttr.muted_) {
+        if (mediaAttr.onHold_)
+            return MediaDirection::INACTIVE;
+        return MediaDirection::RECVONLY;
+    }
+
+    if (mediaAttr.onHold_)
+        return MediaDirection::SENDONLY;
+
+    return MediaDirection::SENDRECV;
+}
+
+uint8_t
+AutoAnswerMediaNegoTest::directionToBitset(MediaDirection direction, bool isLocal)
+{
+    if (direction == MediaDirection::SENDRECV)
+        return 3;
+    if (direction == MediaDirection::RECVONLY)
+        return isLocal ? 2 : 1;
+    if (direction == MediaDirection::SENDONLY)
+        return isLocal ? 1 : 2;
+    return 0;
+}
+
+MediaDirection
+AutoAnswerMediaNegoTest::bitsetToDirection(uint8_t val)
+{
+    if (val == 3)
+        return MediaDirection::SENDRECV;
+    if (val == 2)
+        return MediaDirection::RECVONLY;
+    if (val == 1)
+        return MediaDirection::SENDONLY;
+    return MediaDirection::INACTIVE;
+}
+
+MediaDirection
+AutoAnswerMediaNegoTest::inferNegotiatedDirection(MediaDirection local, MediaDirection remote)
+{
+    uint8_t val = directionToBitset(local, true) & directionToBitset(remote, false);
+    auto dir = bitsetToDirection(val);
+    return dir;
+}
+
+bool
+AutoAnswerMediaNegoTest::validateMuteState(std::vector<MediaAttribute> expected,
+                                           std::vector<MediaAttribute> actual)
+{
+    CPPUNIT_ASSERT_EQUAL(expected.size(), actual.size());
+
+    for (size_t idx = 0; idx < expected.size(); idx++) {
+        if (expected[idx].muted_ != actual[idx].muted_)
+            return false;
+    }
+
+    return true;
+}
+
+bool
+AutoAnswerMediaNegoTest::validateMediaDirection(std::vector<MediaDescription> descrList,
+                                                std::vector<MediaAttribute> localMediaList,
+                                                std::vector<MediaAttribute> remoteMediaList)
+{
+    CPPUNIT_ASSERT_EQUAL(descrList.size(), localMediaList.size());
+    CPPUNIT_ASSERT_EQUAL(descrList.size(), remoteMediaList.size());
+
+    for (size_t idx = 0; idx < descrList.size(); idx++) {
+        auto local = inferInitialDirection(localMediaList[idx]);
+        auto remote = inferInitialDirection(remoteMediaList[idx]);
+        auto negotiated = inferNegotiatedDirection(local, remote);
+
+        if (descrList[idx].direction_ != negotiated) {
+            JAMI_WARN("Media [%lu] direction mismatch: expected %i - found %i",
+                      idx,
+                      static_cast<int>(negotiated),
+                      static_cast<int>(descrList[idx].direction_));
+            return false;
+        }
+    }
+
+    return true;
+}
+
+void
+AutoAnswerMediaNegoTest::onIncomingCallWithMedia(const std::string& accountId,
+                                                 const std::string& callId,
+                                                 const std::vector<DRing::MediaMap> mediaList,
+                                                 CallData& callData)
+{
+    CPPUNIT_ASSERT_EQUAL(callData.accountId_, accountId);
+
+    JAMI_INFO("Signal [%s] - user [%s] - call [%s] - media count [%lu]",
+              DRing::CallSignal::IncomingCallWithMedia::name,
+              callData.alias_.c_str(),
+              callId.c_str(),
+              mediaList.size());
+
+    if (not Manager::instance().getCallFromCallID(callId)) {
+        JAMI_WARN("Call with ID [%s] does not exist!", callId.c_str());
+        callData.callId_ = {};
+        return;
+    }
+
+    std::unique_lock<std::mutex> lock {callData.mtx_};
+    callData.callId_ = callId;
+    callData.signals_.emplace_back(CallData::Signal(DRing::CallSignal::IncomingCallWithMedia::name));
+
+    callData.cv_.notify_one();
+}
+
+void
+AutoAnswerMediaNegoTest::onMediaChangeRequested(const std::string& accountId,
+                                                const std::string& callId,
+                                                const std::vector<DRing::MediaMap> mediaList,
+                                                CallData& callData)
+{
+    CPPUNIT_ASSERT_EQUAL(callData.accountId_, accountId);
+
+    JAMI_INFO("Signal [%s] - user [%s] - call [%s] - media count [%lu]",
+              DRing::CallSignal::MediaChangeRequested::name,
+              callData.alias_.c_str(),
+              callId.c_str(),
+              mediaList.size());
+
+    if (not Manager::instance().getCallFromCallID(callId)) {
+        JAMI_WARN("Call with ID [%s] does not exist!", callId.c_str());
+        callData.callId_ = {};
+        return;
+    }
+
+    std::unique_lock<std::mutex> lock {callData.mtx_};
+    callData.callId_ = callId;
+    callData.signals_.emplace_back(CallData::Signal(DRing::CallSignal::MediaChangeRequested::name));
+
+    callData.cv_.notify_one();
+}
+
+void
+AutoAnswerMediaNegoTest::onCallStateChange(const std::string& accountId UNUSED,
+                                           const std::string& callId,
+                                           const std::string& state,
+                                           CallData& callData)
+{
+    // TODO. rewrite me using accountId.
+
+    auto call = Manager::instance().getCallFromCallID(callId);
+    if (not call) {
+        JAMI_WARN("Call with ID [%s] does not exist anymore!", callId.c_str());
+        return;
+    }
+
+    auto account = call->getAccount().lock();
+    if (not account) {
+        JAMI_WARN("Account owning the call [%s] does not exist!", callId.c_str());
+        return;
+    }
+
+    JAMI_INFO("Signal [%s] - user [%s] - call [%s] - state [%s]",
+              DRing::CallSignal::StateChange::name,
+              callData.alias_.c_str(),
+              callId.c_str(),
+              state.c_str());
+
+    if (account->getAccountID() != callData.accountId_)
+        return;
+
+    {
+        std::unique_lock<std::mutex> lock {callData.mtx_};
+        callData.signals_.emplace_back(
+            CallData::Signal(DRing::CallSignal::StateChange::name, state));
+    }
+
+    if (state == "CURRENT" or state == "OVER" or state == "HUNGUP") {
+        callData.cv_.notify_one();
+    }
+}
+
+void
+AutoAnswerMediaNegoTest::onVideoMuted(const std::string& callId, bool muted, CallData& callData)
+{
+    auto call = Manager::instance().getCallFromCallID(callId);
+
+    if (not call) {
+        JAMI_WARN("Call with ID [%s] does not exist anymore!", callId.c_str());
+        return;
+    }
+
+    auto account = call->getAccount().lock();
+    if (not account) {
+        JAMI_WARN("Account owning the call [%s] does not exist!", callId.c_str());
+        return;
+    }
+
+    JAMI_INFO("Signal [%s] - user [%s] - call [%s] - state [%s]",
+              DRing::CallSignal::VideoMuted::name,
+              account->getAccountDetails()[ConfProperties::ALIAS].c_str(),
+              call->getCallId().c_str(),
+              muted ? "Mute" : "Un-mute");
+
+    if (account->getAccountID() != callData.accountId_)
+        return;
+
+    {
+        std::unique_lock<std::mutex> lock {callData.mtx_};
+        callData.signals_.emplace_back(
+            CallData::Signal(DRing::CallSignal::VideoMuted::name, muted ? "muted" : "un-muted"));
+    }
+
+    callData.cv_.notify_one();
+}
+
+void
+AutoAnswerMediaNegoTest::onMediaNegotiationStatus(const std::string& callId,
+                                                  const std::string& event,
+                                                  CallData& callData)
+{
+    auto call = Manager::instance().getCallFromCallID(callId);
+    if (not call) {
+        JAMI_WARN("Call with ID [%s] does not exist!", callId.c_str());
+        return;
+    }
+
+    auto account = call->getAccount().lock();
+    if (not account) {
+        JAMI_WARN("Account owning the call [%s] does not exist!", callId.c_str());
+        return;
+    }
+
+    JAMI_INFO("Signal [%s] - user [%s] - call [%s] - state [%s]",
+              DRing::CallSignal::MediaNegotiationStatus::name,
+              account->getAccountDetails()[ConfProperties::ALIAS].c_str(),
+              call->getCallId().c_str(),
+              event.c_str());
+
+    if (account->getAccountID() != callData.accountId_)
+        return;
+
+    {
+        std::unique_lock<std::mutex> lock {callData.mtx_};
+        callData.signals_.emplace_back(
+            CallData::Signal(DRing::CallSignal::MediaNegotiationStatus::name, event));
+    }
+
+    callData.cv_.notify_one();
+}
+
+bool
+AutoAnswerMediaNegoTest::waitForSignal(CallData& callData,
+                                       const std::string& expectedSignal,
+                                       const std::string& expectedEvent)
+{
+    const std::chrono::seconds TIME_OUT {15};
+    std::unique_lock<std::mutex> lock {callData.mtx_};
+
+    // Combined signal + event (if any).
+    std::string sigEvent(expectedSignal);
+    if (not expectedEvent.empty())
+        sigEvent += "::" + expectedEvent;
+
+    JAMI_INFO("[%s] is waiting for [%s] signal/event", callData.alias_.c_str(), sigEvent.c_str());
+
+    auto res = callData.cv_.wait_for(lock, TIME_OUT, [&] {
+        // Search for the expected signal in list of received signals.
+        bool pred = false;
+        for (auto it = callData.signals_.begin(); it != callData.signals_.end(); it++) {
+            // The predicate is true if the signal names match, and if the
+            // expectedEvent is not empty, the events must also match.
+            if (it->name_ == expectedSignal
+                and (expectedEvent.empty() or it->event_ == expectedEvent)) {
+                pred = true;
+                // Done with this signal.
+                callData.signals_.erase(it);
+                break;
+            }
+        }
+
+        return pred;
+    });
+
+    if (not res) {
+        JAMI_ERR("[%s] waiting for signal/event [%s] timed-out!",
+                 callData.alias_.c_str(),
+                 sigEvent.c_str());
+
+        JAMI_INFO("[%s] currently has the following signals:", callData.alias_.c_str());
+
+        for (auto const& sig : callData.signals_) {
+            JAMI_INFO() << "Signal [" << sig.name_
+                        << (sig.event_.empty() ? "" : ("::" + sig.event_)) << "]";
+        }
+    }
+
+    return res;
+}
+
+void
+AutoAnswerMediaNegoTest::configureScenario()
+{
+    // Configure Alice
+    {
+        CPPUNIT_ASSERT(not aliceData_.accountId_.empty());
+        auto const& account = Manager::instance().getAccount<Account>(aliceData_.accountId_);
+        aliceData_.userName_ = account->getAccountDetails()[ConfProperties::USERNAME];
+        aliceData_.alias_ = account->getAccountDetails()[ConfProperties::ALIAS];
+        account->enableIceForMedia(true);
+        if (isSipAccount_) {
+            auto sipAccount = std::dynamic_pointer_cast<SIPAccount>(account);
+            CPPUNIT_ASSERT(sipAccount);
+            sipAccount->setLocalPort(aliceData_.listeningPort_);
+        }
+    }
+
+    // Configure Bob
+    {
+        CPPUNIT_ASSERT(not bobData_.accountId_.empty());
+        auto const& account = Manager::instance().getAccount<Account>(bobData_.accountId_);
+        bobData_.userName_ = account->getAccountDetails()[ConfProperties::USERNAME];
+        bobData_.alias_ = account->getAccountDetails()[ConfProperties::ALIAS];
+        account->enableIceForMedia(true);
+        account->isAutoAnswerEnabled();
+
+        if (isSipAccount_) {
+            auto sipAccount = std::dynamic_pointer_cast<SIPAccount>(account);
+            CPPUNIT_ASSERT(sipAccount);
+            sipAccount->setLocalPort(bobData_.listeningPort_);
+            bobData_.toUri_ = "127.0.0.1:" + std::to_string(bobData_.listeningPort_);
+        }
+    }
+
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> signalHandlers;
+
+    // Insert needed signal handlers.
+    signalHandlers.insert(DRing::exportable_callback<DRing::CallSignal::IncomingCallWithMedia>(
+        [&](const std::string& accountId,
+            const std::string& callId,
+            const std::string&,
+            const std::vector<DRing::MediaMap> mediaList) {
+            auto user = getUserAlias(callId);
+            if (not user.empty())
+                onIncomingCallWithMedia(accountId,
+                                        callId,
+                                        mediaList,
+                                        user == aliceData_.alias_ ? aliceData_ : bobData_);
+        }));
+
+    signalHandlers.insert(DRing::exportable_callback<DRing::CallSignal::MediaChangeRequested>(
+        [&](const std::string& accountId,
+            const std::string& callId,
+            const std::vector<DRing::MediaMap> mediaList) {
+            auto user = getUserAlias(callId);
+            if (not user.empty())
+                onMediaChangeRequested(accountId,
+                                       callId,
+                                       mediaList,
+                                       user == aliceData_.alias_ ? aliceData_ : bobData_);
+        }));
+
+    signalHandlers.insert(
+        DRing::exportable_callback<DRing::CallSignal::StateChange>([&](const std::string& accountId,
+                                                                       const std::string& callId,
+                                                                       const std::string& state,
+                                                                       signed) {
+            auto user = getUserAlias(callId);
+            if (not user.empty())
+                onCallStateChange(accountId,
+                                  callId,
+                                  state,
+                                  user == aliceData_.alias_ ? aliceData_ : bobData_);
+        }));
+
+    signalHandlers.insert(DRing::exportable_callback<DRing::CallSignal::VideoMuted>(
+        [&](const std::string& callId, bool muted) {
+            auto user = getUserAlias(callId);
+            if (not user.empty())
+                onVideoMuted(callId, muted, user == aliceData_.alias_ ? aliceData_ : bobData_);
+        }));
+
+    signalHandlers.insert(DRing::exportable_callback<DRing::CallSignal::MediaNegotiationStatus>(
+        [&](const std::string& callId,
+            const std::string& event,
+            const std::vector<std::map<std::string, std::string>>&) {
+            auto user = getUserAlias(callId);
+            if (not user.empty())
+                onMediaNegotiationStatus(callId,
+                                         event,
+                                         user == aliceData_.alias_ ? aliceData_ : bobData_);
+        }));
+
+    DRing::registerSignalHandlers(signalHandlers);
+}
+
+void
+AutoAnswerMediaNegoTest::testWithScenario(CallData& aliceData,
+                                          CallData& bobData,
+                                          const TestScenario& scenario)
+{
+    JAMI_INFO("=== Start a call and validate ===");
+
+    // The media count of the offer and answer must match (RFC-3264).
+    auto mediaCount = scenario.offer_.size();
+    CPPUNIT_ASSERT_EQUAL(mediaCount, scenario.answer_.size());
+
+    aliceData.callId_ = DRing::placeCallWithMedia(aliceData.accountId_,
+                                                  isSipAccount_ ? bobData.toUri_
+                                                                : bobData_.userName_,
+                                                  MediaAttribute::mediaAttributesToMediaMaps(
+                                                      scenario.offer_));
+    CPPUNIT_ASSERT(not aliceData.callId_.empty());
+    auto aliceCall = std::static_pointer_cast<SIPCall>(
+        Manager::instance().getCallFromCallID(aliceData.callId_));
+
+    CPPUNIT_ASSERT(aliceCall);
+
+    JAMI_INFO("ALICE [%s] started a call with BOB [%s] and wait for answer",
+              aliceData.accountId_.c_str(),
+              bobData.accountId_.c_str());
+
+    // Wait for incoming call signal.
+    CPPUNIT_ASSERT(waitForSignal(bobData, DRing::CallSignal::IncomingCallWithMedia::name));
+
+    // Bob automatically answers the call.
+
+    // Wait for media negotiation complete signal.
+    CPPUNIT_ASSERT_EQUAL(
+        true,
+        waitForSignal(bobData,
+                      DRing::CallSignal::MediaNegotiationStatus::name,
+                      DRing::Media::MediaNegotiationStatusEvents::NEGOTIATION_SUCCESS));
+    // Wait for the StateChange signal.
+    CPPUNIT_ASSERT_EQUAL(true,
+                         waitForSignal(bobData,
+                                       DRing::CallSignal::StateChange::name,
+                                       StateEvent::CURRENT));
+
+    JAMI_INFO("BOB answered the call [%s]", bobData.callId_.c_str());
+
+    // Wait for media negotiation complete signal.
+    CPPUNIT_ASSERT_EQUAL(
+        true,
+        waitForSignal(aliceData,
+                      DRing::CallSignal::MediaNegotiationStatus::name,
+                      DRing::Media::MediaNegotiationStatusEvents::NEGOTIATION_SUCCESS));
+
+    // Validate Alice's media
+    {
+        auto mediaList = aliceCall->getMediaAttributeList();
+        CPPUNIT_ASSERT_EQUAL(mediaCount, mediaList.size());
+
+        // Validate mute state
+        CPPUNIT_ASSERT(validateMuteState(scenario.offer_, mediaList));
+
+        auto& sdp = aliceCall->getSDP();
+
+        // Validate local media direction
+        {
+            auto descrList = sdp.getActiveMediaDescription(false);
+            CPPUNIT_ASSERT_EQUAL(mediaCount, descrList.size());
+            // For Alice, local is the offer and remote is the answer.
+            CPPUNIT_ASSERT(validateMediaDirection(descrList, scenario.offer_, scenario.answer_));
+        }
+    }
+
+    // Validate Bob's media
+    {
+        auto const& bobCall = std::dynamic_pointer_cast<SIPCall>(
+            Manager::instance().getCallFromCallID(bobData.callId_));
+        auto mediaList = bobCall->getMediaAttributeList();
+        CPPUNIT_ASSERT_EQUAL(mediaCount, mediaList.size());
+
+        // Validate mute state
+        CPPUNIT_ASSERT(validateMuteState(scenario.answer_, mediaList));
+
+        auto& sdp = bobCall->getSDP();
+
+        // Validate local media direction
+        {
+            auto descrList = sdp.getActiveMediaDescription(false);
+            CPPUNIT_ASSERT_EQUAL(mediaCount, descrList.size());
+            // For Bob, local is the answer and remote is the offer.
+            CPPUNIT_ASSERT(validateMediaDirection(descrList, scenario.answer_, scenario.offer_));
+        }
+    }
+
+    std::this_thread::sleep_for(std::chrono::seconds(3));
+
+    JAMI_INFO("=== Request Media Change and validate ===");
+    {
+        auto const& mediaList = MediaAttribute::mediaAttributesToMediaMaps(scenario.offerUpdate_);
+        DRing::requestMediaChange(aliceData.accountId_, aliceData.callId_, mediaList);
+    }
+
+    // Update and validate media count.
+    mediaCount = scenario.offerUpdate_.size();
+    CPPUNIT_ASSERT_EQUAL(mediaCount, scenario.answerUpdate_.size());
+
+    if (scenario.expectMediaRenegotiation_) {
+        // Wait for media negotiation complete signal.
+        CPPUNIT_ASSERT_EQUAL(
+            true,
+            waitForSignal(aliceData,
+                          DRing::CallSignal::MediaNegotiationStatus::name,
+                          DRing::Media::MediaNegotiationStatusEvents::NEGOTIATION_SUCCESS));
+
+        // Validate Alice's media
+        {
+            auto mediaList = aliceCall->getMediaAttributeList();
+            CPPUNIT_ASSERT_EQUAL(mediaCount, mediaList.size());
+
+            // Validate mute state
+            CPPUNIT_ASSERT(validateMuteState(scenario.offerUpdate_, mediaList));
+
+            auto& sdp = aliceCall->getSDP();
+
+            // Validate local media direction
+            {
+                auto descrList = sdp.getActiveMediaDescription(false);
+                CPPUNIT_ASSERT_EQUAL(mediaCount, descrList.size());
+                CPPUNIT_ASSERT(validateMediaDirection(descrList,
+                                                      scenario.offerUpdate_,
+                                                      scenario.answerUpdate_));
+            }
+            // Validate remote media direction
+            {
+                auto descrList = sdp.getActiveMediaDescription(true);
+                CPPUNIT_ASSERT_EQUAL(mediaCount, descrList.size());
+                CPPUNIT_ASSERT(validateMediaDirection(descrList,
+                                                      scenario.answerUpdate_,
+                                                      scenario.offerUpdate_));
+            }
+        }
+
+        // Validate Bob's media
+        {
+            auto const& bobCall = std::dynamic_pointer_cast<SIPCall>(
+                Manager::instance().getCallFromCallID(bobData.callId_));
+            auto mediaList = bobCall->getMediaAttributeList();
+            CPPUNIT_ASSERT_EQUAL(mediaCount, mediaList.size());
+
+            // Validate mute state
+            CPPUNIT_ASSERT(validateMuteState(scenario.answerUpdate_, mediaList));
+
+            // NOTE:
+            // It should be enough to validate media direction on Alice's side
+        }
+    }
+
+    std::this_thread::sleep_for(std::chrono::seconds(3));
+
+    // Bob hang-up.
+    JAMI_INFO("Hang up BOB's call and wait for ALICE to hang up");
+    DRing::hangUp(bobData.accountId_, bobData.callId_);
+
+    CPPUNIT_ASSERT_EQUAL(true,
+                         waitForSignal(aliceData,
+                                       DRing::CallSignal::StateChange::name,
+                                       StateEvent::HUNGUP));
+
+    JAMI_INFO("Call terminated on both sides");
+}
+
+void
+AutoAnswerMediaNegoTest::audio_and_video_then_caller_mute_video()
+{
+    JAMI_INFO("=== Begin test %s ===", __FUNCTION__);
+
+    configureScenario();
+
+    MediaAttribute defaultAudio(MediaType::MEDIA_AUDIO);
+    defaultAudio.label_ = "audio_0";
+    defaultAudio.enabled_ = true;
+
+    MediaAttribute defaultVideo(MediaType::MEDIA_VIDEO);
+    defaultVideo.label_ = "video_0";
+    defaultVideo.enabled_ = true;
+
+    MediaAttribute audio(defaultAudio);
+    MediaAttribute video(defaultVideo);
+
+    TestScenario scenario;
+    // First offer/answer
+    scenario.offer_.emplace_back(audio);
+    scenario.offer_.emplace_back(video);
+    scenario.answer_.emplace_back(audio);
+    scenario.answer_.emplace_back(video);
+
+    // Updated offer/answer
+    scenario.offerUpdate_.emplace_back(audio);
+    video.muted_ = true;
+    scenario.offerUpdate_.emplace_back(video);
+
+    scenario.answerUpdate_.emplace_back(audio);
+    video.muted_ = false;
+    scenario.answerUpdate_.emplace_back(video);
+    scenario.expectMediaRenegotiation_ = true;
+
+    testWithScenario(aliceData_, bobData_, scenario);
+
+    DRing::unregisterSignalHandlers();
+
+    JAMI_INFO("=== End test %s ===", __FUNCTION__);
+}
+
+void
+AutoAnswerMediaNegoTest::audio_only_then_caller_add_video()
+{
+    JAMI_INFO("=== Begin test %s ===", __FUNCTION__);
+
+    configureScenario();
+
+    MediaAttribute defaultAudio(MediaType::MEDIA_AUDIO);
+    defaultAudio.label_ = "audio_0";
+    defaultAudio.enabled_ = true;
+
+    MediaAttribute defaultVideo(MediaType::MEDIA_VIDEO);
+    defaultVideo.label_ = "video_0";
+    defaultVideo.enabled_ = true;
+
+    MediaAttribute audio(defaultAudio);
+    MediaAttribute video(defaultVideo);
+
+    TestScenario scenario;
+    // First offer/answer
+    scenario.offer_.emplace_back(audio);
+    scenario.answer_.emplace_back(audio);
+
+    // Updated offer/answer
+    scenario.offerUpdate_.emplace_back(audio);
+    scenario.offerUpdate_.emplace_back(video);
+    scenario.answerUpdate_.emplace_back(audio);
+    scenario.answerUpdate_.emplace_back(video);
+    scenario.expectMediaRenegotiation_ = true;
+
+    testWithScenario(aliceData_, bobData_, scenario);
+
+    DRing::unregisterSignalHandlers();
+
+    JAMI_INFO("=== End test %s ===", __FUNCTION__);
+}
+
+void
+AutoAnswerMediaNegoTest::audio_and_video_then_caller_mute_audio()
+{
+    JAMI_INFO("=== Begin test %s ===", __FUNCTION__);
+
+    configureScenario();
+
+    MediaAttribute defaultAudio(MediaType::MEDIA_AUDIO);
+    defaultAudio.label_ = "audio_0";
+    defaultAudio.enabled_ = true;
+
+    MediaAttribute defaultVideo(MediaType::MEDIA_VIDEO);
+    defaultVideo.label_ = "video_0";
+    defaultVideo.enabled_ = true;
+
+    MediaAttribute audio(defaultAudio);
+    MediaAttribute video(defaultVideo);
+
+    TestScenario scenario;
+    // First offer/answer
+    scenario.offer_.emplace_back(audio);
+    scenario.offer_.emplace_back(video);
+    scenario.answer_.emplace_back(audio);
+    scenario.answer_.emplace_back(video);
+
+    // Updated offer/answer
+    audio.muted_ = true;
+    scenario.offerUpdate_.emplace_back(audio);
+    scenario.offerUpdate_.emplace_back(video);
+
+    audio.muted_ = false;
+    scenario.answerUpdate_.emplace_back(audio);
+    scenario.answerUpdate_.emplace_back(video);
+
+    scenario.expectMediaRenegotiation_ = false;
+
+    testWithScenario(aliceData_, bobData_, scenario);
+
+    DRing::unregisterSignalHandlers();
+
+    JAMI_INFO("=== End test %s ===", __FUNCTION__);
+}
+
+void
+AutoAnswerMediaNegoTest::audio_and_video_then_change_video_source()
+{
+    JAMI_INFO("=== Begin test %s ===", __FUNCTION__);
+
+    configureScenario();
+
+    MediaAttribute defaultAudio(MediaType::MEDIA_AUDIO);
+    defaultAudio.label_ = "audio_0";
+    defaultAudio.enabled_ = true;
+
+    MediaAttribute defaultVideo(MediaType::MEDIA_VIDEO);
+    defaultVideo.label_ = "video_0";
+    defaultVideo.enabled_ = true;
+
+    MediaAttribute audio(defaultAudio);
+    MediaAttribute video(defaultVideo);
+
+    TestScenario scenario;
+    // First offer/answer
+    scenario.offer_.emplace_back(audio);
+    scenario.offer_.emplace_back(video);
+    scenario.answer_.emplace_back(audio);
+    scenario.answer_.emplace_back(video);
+
+    // Updated offer/answer
+    scenario.offerUpdate_.emplace_back(audio);
+    // Just change the media source to validate that a new
+    // media negotiation (re-invite) will be triggered.
+    video.sourceUri_ = "Fake source";
+    scenario.offerUpdate_.emplace_back(video);
+
+    scenario.answerUpdate_.emplace_back(audio);
+    scenario.answerUpdate_.emplace_back(video);
+
+    scenario.expectMediaRenegotiation_ = true;
+
+    testWithScenario(aliceData_, bobData_, scenario);
+
+    DRing::unregisterSignalHandlers();
+
+    JAMI_INFO("=== End test %s ===", __FUNCTION__);
+}
+
+CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(AutoAnswerMediaNegoTestSip, "AutoAnswerMediaNegoTestSip");
+CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(AutoAnswerMediaNegoTestJami, "AutoAnswerMediaNegoTestJami");
+
+} // namespace test
+} // namespace jami
+
+JAMI_TEST_RUNNER(jami::test::AutoAnswerMediaNegoTestJami::name())
diff --git a/test/unitTest/media_negotiation/media_negotiation.cpp b/test/unitTest/media_negotiation/media_negotiation.cpp
index 8d3e8fbef0..bd65ec51c2 100644
--- a/test/unitTest/media_negotiation/media_negotiation.cpp
+++ b/test/unitTest/media_negotiation/media_negotiation.cpp
@@ -147,7 +147,9 @@ private:
 
     // Helpers
     static void configureScenario(CallData& bob, CallData& alice);
-    void testWithScenario(CallData& aliceData, CallData& bobData, const TestScenario& scenario);
+    static void testWithScenario(CallData& aliceData,
+                                 CallData& bobData,
+                                 const TestScenario& scenario);
     static std::string getUserAlias(const std::string& callId);
     // Infer media direction of an offer.
     static uint8_t directionToBitset(MediaDirection direction, bool isLocal);
@@ -666,7 +668,7 @@ MediaNegotiationTest::testWithScenario(CallData& aliceData,
     // Answer the call.
     {
         auto const& mediaList = MediaAttribute::mediaAttributesToMediaMaps(scenario.answer_);
-        Manager::instance().answerCall(bobData.accountId_, bobData.callId_, mediaList);
+        DRing::acceptWithMedia(bobData.accountId_, bobData.callId_, mediaList);
     }
 
     // Wait for media negotiation complete signal.
@@ -798,7 +800,7 @@ MediaNegotiationTest::testWithScenario(CallData& aliceData,
 
     // Bob hang-up.
     JAMI_INFO("Hang up BOB's call and wait for ALICE to hang up");
-    Manager::instance().hangupCall(bobData.accountId_, bobData.callId_);
+    DRing::hangUp(bobData.accountId_, bobData.callId_);
 
     CPPUNIT_ASSERT_EQUAL(true,
                          waitForSignal(aliceData,
@@ -1026,4 +1028,4 @@ MediaNegotiationTest::audio_and_video_then_change_video_source()
 } // namespace test
 } // namespace jami
 
-RING_TEST_RUNNER(jami::test::MediaNegotiationTest::name())
+JAMI_TEST_RUNNER(jami::test::MediaNegotiationTest::name())
-- 
GitLab