diff --git a/src/observer.h b/src/observer.h
index 03619f0516aba8463ac01356cc69ad9002186e5c..e32cfe789aa4de8d0ce360b3141a037517826508 100644
--- a/src/observer.h
+++ b/src/observer.h
@@ -31,7 +31,9 @@
 #include <mutex>
 #include <functional>
 #include <ciso646> // fix windows compiler bug
+#ifndef __DEBUG__ // this is only defined on plugins build for debugging
 #include "logger.h"
+#endif
 
 namespace jami {
 
@@ -127,7 +129,9 @@ protected:
                 try {
                     so->update(this, data);
                 } catch (std::exception& e) {
+#ifndef __DEBUG__
                     JAMI_ERR() << e.what();
+#endif
                 }
             } else {
                 it = priority_observers_.erase(it);
diff --git a/src/plugin/streamdata.h b/src/plugin/streamdata.h
index e0d51bd40efd93929f77e5150ee563887e1ba5b6..3cd7f572a380aac7becaf08ea40ccee6c5f9fe12 100644
--- a/src/plugin/streamdata.h
+++ b/src/plugin/streamdata.h
@@ -70,7 +70,7 @@ struct JamiMessage
     /**
      * @param accId AccountId
      * @param pId peerId
-     * @param isReceived False if local audio/video streams
+     * @param isReceived True if received message, False if sent
      * @param dataMap Message contents
      * @param pPlugin True if message is created/modified by plugin code
      */
diff --git a/test/meson.build b/test/meson.build
index cce4ceb02c0cba2ba465fcb65084342b6c5663b3..edb5421e6a163b8f69f7e32b74fac719af2cf7a2 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -452,4 +452,15 @@ if conf.get('ENABLE_VIDEO')
     test('video_scaler', ut_video_scaler,
         workdir: ut_workdir, is_parallel: false, timeout: 1800
     )
+
+
+    ut_plugins = executable('ut_plugins',
+        sources: files('unitTest/plugins/plugins.cpp'),
+        include_directories: ut_includedirs,
+        dependencies: ut_dependencies,
+        link_with: ut_library
+    )
+    test('plugins', ut_plugins,
+        workdir: ut_workdir, is_parallel: false, timeout: 1800
+    )
 endif
diff --git a/test/unitTest/Makefile.am b/test/unitTest/Makefile.am
index 05c9e46666536a45f57e5064b188458ecd605983..7e87bbf788610ae66c1054b4a7c83f935d97b18d 100644
--- a/test/unitTest/Makefile.am
+++ b/test/unitTest/Makefile.am
@@ -226,5 +226,10 @@ ut_sip_empty_offer_SOURCES = sip_account/sip_empty_offer.cpp
 check_PROGRAMS += ut_sip_srtp
 ut_sip_srtp_SOURCES = sip_account/sip_srtp.cpp
 
+#
+# Plugins
+#
+check_PROGRAMS += ut_plugins
+ut_plugins_SOURCES = plugins/plugins.cpp common.cpp
 
 TESTS = $(check_PROGRAMS)
diff --git a/test/unitTest/plugins/README b/test/unitTest/plugins/README
new file mode 100644
index 0000000000000000000000000000000000000000..61f536ca1aa35aabd3a703aed6f9b1ff0ecfef1d
--- /dev/null
+++ b/test/unitTest/plugins/README
@@ -0,0 +1,42 @@
+COPYRIGHT NOTICE
+
+Copyright (C) 2022 Savoir-faire Linux Inc.
+
+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 <http://www.gnu.org/licenses/>.
+
+plugin.yml
+----------
+
+The test configuration file can specify:
+
+* jplDirectory - relative to the test executable or a full path;
+
+* plugin - the plugin name;
+
+* mediaHandlers - present media handlers names in the plugin;
+
+* chatHandlers - present chat handlers names in the plugin;
+
+The default test plugin is TestSuite.
+
+Test plugin build
+-----------------
+
+For the CI tests, which uses a Ubuntu 20.04 docker, the test suite must be build with an appropriate glibc version, meaning a glib 3.31 or older.
+
+Jami supports systems from Ubuntu 18.04, which uses glib 2.27.
+
+If the plugin is build within a Ubuntu 18.04, it should work on all Jami supported platforms.
+
+TO check your system glib version: `ldd --version`
diff --git a/test/unitTest/plugins/TestSuite.jpl b/test/unitTest/plugins/TestSuite.jpl
new file mode 100644
index 0000000000000000000000000000000000000000..2fa78b959f8594ead7c23597401688ed5832199f
Binary files /dev/null and b/test/unitTest/plugins/TestSuite.jpl differ
diff --git a/test/unitTest/plugins/plugin.yml b/test/unitTest/plugins/plugin.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3971061408614eea6e783329691f107764311ab9
--- /dev/null
+++ b/test/unitTest/plugins/plugin.yml
@@ -0,0 +1,9 @@
+jplDirectory:
+  "plugins"
+plugin:
+  "TestSuite"
+mediaHandlers:
+  - "AudioHandlerTester"
+  - "VideoHandlerTester"
+chatHandlers:
+  - "ChatHandlerTester"
diff --git a/test/unitTest/plugins/plugins.cpp b/test/unitTest/plugins/plugins.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a74a1b87becb2a074c94c27f828a149ce99b7950
--- /dev/null
+++ b/test/unitTest/plugins/plugins.cpp
@@ -0,0 +1,665 @@
+/*
+ *  Copyright (C) 2022 Savoir-faire Linux Inc.
+ *  Author: Aline Gondim Santos <aline.gondimsantos@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 <filesystem>
+#include <string>
+
+#include "manager.h"
+#include "plugin/jamipluginmanager.h"
+#include "jamidht/jamiaccount.h"
+#include "../../test_runner.h"
+#include "jami.h"
+#include "fileutils.h"
+#include "jami/media_const.h"
+#include "account_const.h"
+#include "sip/sipcall.h"
+#include "call_const.h"
+
+#include "common.h"
+
+using namespace DRing::Account;
+
+namespace jami {
+namespace test {
+
+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_ {};
+    };
+
+    CallData() = default;
+    CallData(CallData&& other) = delete;
+    CallData(const CallData& other)
+    {
+        accountId_ = std::move(other.accountId_);
+        listeningPort_ = other.listeningPort_;
+        userName_ = std::move(other.userName_);
+        alias_ = std::move(other.alias_);
+        callId_ = std::move(other.callId_);
+        signals_ = std::move(other.signals_);
+    };
+
+    std::string accountId_ {};
+    std::string userName_ {};
+    std::string alias_ {};
+    uint16_t listeningPort_ {0};
+    std::string toUri_ {};
+    std::string callId_ {};
+    std::vector<Signal> signals_;
+    std::condition_variable cv_ {};
+    std::mutex mtx_;
+};
+
+class PluginsTest : public CppUnit::TestFixture
+{
+public:
+    PluginsTest()
+    {
+        // 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"));
+    }
+    ~PluginsTest() { DRing::fini(); }
+    static std::string name() { return "Plugins"; }
+    void setUp();
+    void tearDown();
+
+    CallData aliceData;
+    CallData bobData;
+
+private:
+    static bool waitForSignal(CallData& callData,
+                              const std::string& signal,
+                              const std::string& expectedEvent = {});
+    // 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);
+
+    std::string name_{};
+    std::string jplPath_{};
+    std::string installationPath_{};
+    std::vector<std::string> mediaHandlers_{};
+    std::vector<std::string> chatHandlers_{};
+
+    void testEnable();
+    void testInstallAndLoad();
+    void testHandlers();
+    void testDetailsAndPreferences();
+    void testCall();
+    void testMessage();
+
+    CPPUNIT_TEST_SUITE(PluginsTest);
+    CPPUNIT_TEST(testEnable);
+    CPPUNIT_TEST(testInstallAndLoad);
+    CPPUNIT_TEST(testHandlers);
+    CPPUNIT_TEST(testDetailsAndPreferences);
+    CPPUNIT_TEST(testCall);
+    CPPUNIT_TEST(testMessage);
+    CPPUNIT_TEST_SUITE_END();
+};
+
+CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(PluginsTest, PluginsTest::name());
+
+void
+PluginsTest::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());
+
+    // NOTE.
+    // We shouldn't access shared_ptr<Call> as this event is supposed to mimic
+    // the client, and the client have no access to this type. But here, we only
+    // needed to check if the call exists. This is the most straightforward and
+    // reliable way to do it until we add a new API (like hasCall(id)).
+    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
+PluginsTest::onCallStateChange(const std::string& accountId,
+                                const std::string& callId,
+                                const std::string& state,
+                                CallData& callData)
+{
+    JAMI_INFO("Signal [%s] - user [%s] - call [%s] - state [%s]",
+              DRing::CallSignal::StateChange::name,
+              callData.alias_.c_str(),
+              callId.c_str(),
+              state.c_str());
+
+    CPPUNIT_ASSERT(accountId == callData.accountId_);
+
+    {
+        std::unique_lock<std::mutex> lock {callData.mtx_};
+        callData.signals_.emplace_back(
+            CallData::Signal(DRing::CallSignal::StateChange::name, state));
+    }
+    // NOTE. Only states that we are interested in will notify the CV.
+    // If this unit test is modified to process other states, they must
+    // be added here.
+    if (state == "CURRENT" or state == "OVER" or state == "HUNGUP" or state == "RINGING") {
+        callData.cv_.notify_one();
+    }
+}
+
+void
+PluginsTest::setUp()
+{
+    auto actors = load_actors_and_wait_for_announcement("actors/alice-bob-no-upnp.yml");
+
+    aliceData.accountId_ = actors["alice"];
+    bobData.accountId_ = actors["bob"];
+
+    // 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);
+    }
+
+    // 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);
+    }
+
+    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) {
+            if (aliceData.accountId_ == accountId)
+                onIncomingCallWithMedia(accountId, callId, mediaList, aliceData);
+            else if (bobData.accountId_ == accountId)
+                onIncomingCallWithMedia(accountId, callId, mediaList, bobData);
+        }));
+
+    signalHandlers.insert(
+        DRing::exportable_callback<DRing::CallSignal::StateChange>([&](const std::string& accountId,
+                                                                       const std::string& callId,
+                                                                       const std::string& state,
+                                                                       signed) {
+            if (aliceData.accountId_ == accountId)
+                onCallStateChange(accountId, callId, state, aliceData);
+            else if (bobData.accountId_ == accountId)
+                onCallStateChange(accountId, callId, state, bobData);
+        }));
+
+    DRing::registerSignalHandlers(signalHandlers);
+
+    std::ifstream file = jami::fileutils::ifstream("plugins/plugin.yml");
+    assert(file.is_open());
+    YAML::Node node = YAML::Load(file);
+
+    assert(node.IsMap());
+
+    name_ = node["plugin"].as<std::string>();
+    jplPath_ = node["jplDirectory"].as<std::string>() + DIR_SEPARATOR_CH + name_ + ".jpl";
+    installationPath_ = fileutils::get_data_dir() + DIR_SEPARATOR_CH + "plugins" + DIR_SEPARATOR_CH + name_;
+    mediaHandlers_ = node["mediaHandlers"].as<std::vector<std::string>>();
+    chatHandlers_ = node["chatHandlers"].as<std::vector<std::string>>();
+}
+
+void
+PluginsTest::tearDown()
+{
+    DRing::unregisterSignalHandlers();
+    wait_for_removal_of({aliceData.accountId_, bobData.accountId_});
+}
+
+void
+PluginsTest::testEnable()
+{
+    Manager::instance().pluginPreferences.setPluginsEnabled(true);
+    CPPUNIT_ASSERT(Manager::instance().pluginPreferences.getPluginsEnabled());
+    Manager::instance().pluginPreferences.setPluginsEnabled(false);
+    CPPUNIT_ASSERT(!Manager::instance().pluginPreferences.getPluginsEnabled());
+}
+
+void
+PluginsTest::testInstallAndLoad()
+{
+    Manager::instance().pluginPreferences.setPluginsEnabled(true);
+
+    CPPUNIT_ASSERT(!Manager::instance().getJamiPluginManager().installPlugin(jplPath_, true));
+    auto installedPlugins = Manager::instance().getJamiPluginManager().getInstalledPlugins();
+    CPPUNIT_ASSERT(!installedPlugins.empty());
+    CPPUNIT_ASSERT(std::find(installedPlugins.begin(),
+                             installedPlugins.end(),
+                             installationPath_)
+                   != installedPlugins.end());
+
+    auto loadedPlugins = Manager::instance().getJamiPluginManager().getLoadedPlugins();
+    CPPUNIT_ASSERT(!loadedPlugins.empty());
+    CPPUNIT_ASSERT(std::find(loadedPlugins.begin(),
+                             loadedPlugins.end(),
+                             installationPath_)
+                   != loadedPlugins.end());
+
+    CPPUNIT_ASSERT(Manager::instance().getJamiPluginManager().unloadPlugin(installationPath_));
+    loadedPlugins = Manager::instance().getJamiPluginManager().getLoadedPlugins();
+    CPPUNIT_ASSERT(std::find(loadedPlugins.begin(),
+                             loadedPlugins.end(),
+                             installationPath_)
+                   == loadedPlugins.end());
+
+    CPPUNIT_ASSERT(!Manager::instance().getJamiPluginManager().uninstallPlugin(installationPath_));
+    installedPlugins = Manager::instance().getJamiPluginManager().getInstalledPlugins();
+    CPPUNIT_ASSERT(std::find(installedPlugins.begin(),
+                             installedPlugins.end(),
+                             installationPath_)
+                   == installedPlugins.end());
+
+}
+
+void
+PluginsTest::testHandlers()
+{
+    Manager::instance().pluginPreferences.setPluginsEnabled(true);
+
+    Manager::instance().getJamiPluginManager().installPlugin(jplPath_, true);
+
+    auto mediaHandlers = Manager::instance().getJamiPluginManager().getCallServicesManager().getCallMediaHandlers();
+    auto chatHandlers = Manager::instance().getJamiPluginManager().getChatServicesManager().getChatHandlers();
+
+    auto handlerLoaded = mediaHandlers_.size() + chatHandlers_.size(); // number of handlers expected
+    for (auto handler : mediaHandlers)
+    {
+        auto details = Manager::instance().getJamiPluginManager().getCallServicesManager().getCallMediaHandlerDetails(handler);
+        // check details expected for the test plugin
+        if(std::find(mediaHandlers_.begin(),
+                        mediaHandlers_.end(),
+                        details["name"])
+                   != mediaHandlers_.end()) {
+            handlerLoaded--;
+        }
+    }
+    for (auto handler : chatHandlers)
+    {
+        auto details = Manager::instance().getJamiPluginManager().getChatServicesManager().getChatHandlerDetails(handler);
+        // check details expected for the test plugin
+        if(std::find(chatHandlers_.begin(),
+                        chatHandlers_.end(),
+                        details["name"])
+                   != chatHandlers_.end()) {
+            handlerLoaded--;
+        }
+    }
+
+    CPPUNIT_ASSERT(!handlerLoaded); // All expected handlers were found
+    CPPUNIT_ASSERT(!Manager::instance().getJamiPluginManager().uninstallPlugin(installationPath_));
+}
+
+void
+PluginsTest::testDetailsAndPreferences()
+{
+    Manager::instance().pluginPreferences.setPluginsEnabled(true);
+    Manager::instance().getJamiPluginManager().installPlugin(jplPath_, true);
+    // Unload now to avoid reloads when changing the preferences
+    Manager::instance().getJamiPluginManager().unloadPlugin(installationPath_);
+
+    // Details
+    auto details = Manager::instance().getJamiPluginManager().getPluginDetails(installationPath_);
+    CPPUNIT_ASSERT(details["name"] == name_);
+
+    // Get-set-reset - no account
+    auto preferences = Manager::instance().getJamiPluginManager().getPluginPreferences(installationPath_, "");
+    CPPUNIT_ASSERT(!preferences.empty());
+    auto preferencesValuesOrig = Manager::instance().getJamiPluginManager().getPluginPreferencesValuesMap(installationPath_, "");
+
+    std::string preferenceNewValue = aliceData.accountId_;
+    auto key = preferences[0]["key"];
+    CPPUNIT_ASSERT(Manager::instance().getJamiPluginManager().setPluginPreference(installationPath_, "", key, preferenceNewValue));
+
+    // Test global preference change
+    auto preferencesValuesNew = Manager::instance().getJamiPluginManager().getPluginPreferencesValuesMap(installationPath_, "");
+    CPPUNIT_ASSERT(preferencesValuesOrig[key] != preferencesValuesNew[key]);
+    CPPUNIT_ASSERT(preferencesValuesNew[key] == preferenceNewValue);
+
+    // Test global preference change in an account
+    preferencesValuesNew = Manager::instance().getJamiPluginManager().getPluginPreferencesValuesMap(installationPath_, aliceData.accountId_);
+    CPPUNIT_ASSERT(preferencesValuesOrig[key] != preferencesValuesNew[key]);
+    CPPUNIT_ASSERT(preferencesValuesNew[key] == preferenceNewValue);
+
+    // Test reset global preference change
+    Manager::instance().getJamiPluginManager().resetPluginPreferencesValuesMap(installationPath_, "");
+    preferencesValuesNew = Manager::instance().getJamiPluginManager().getPluginPreferencesValuesMap(installationPath_, "");
+    CPPUNIT_ASSERT(preferencesValuesOrig[key] == preferencesValuesNew[key]);
+    CPPUNIT_ASSERT(preferencesValuesNew[key] != preferenceNewValue);
+
+
+
+    // Get-set-reset - alice account
+    preferences = Manager::instance().getJamiPluginManager().getPluginPreferences(installationPath_, aliceData.accountId_);
+    CPPUNIT_ASSERT(!preferences.empty());
+    preferencesValuesOrig = Manager::instance().getJamiPluginManager().getPluginPreferencesValuesMap(installationPath_, aliceData.accountId_);
+    auto preferencesValuesBobOrig = Manager::instance().getJamiPluginManager().getPluginPreferencesValuesMap(installationPath_, bobData.accountId_);
+
+    key = preferences[0]["key"];
+    CPPUNIT_ASSERT(Manager::instance().getJamiPluginManager().setPluginPreference(installationPath_, aliceData.accountId_, key, preferenceNewValue));
+
+    // Test account preference change
+    preferencesValuesNew = Manager::instance().getJamiPluginManager().getPluginPreferencesValuesMap(installationPath_, aliceData.accountId_);
+    auto preferencesValuesBobNew = Manager::instance().getJamiPluginManager().getPluginPreferencesValuesMap(installationPath_, bobData.accountId_);
+    CPPUNIT_ASSERT(preferencesValuesBobNew[key] == preferencesValuesBobOrig[key]);
+    CPPUNIT_ASSERT(preferencesValuesOrig[key] != preferencesValuesNew[key]);
+    CPPUNIT_ASSERT(preferencesValuesOrig[key] == preferencesValuesBobOrig[key]);
+    CPPUNIT_ASSERT(preferencesValuesNew[key] != preferencesValuesBobOrig[key]);
+    CPPUNIT_ASSERT(preferencesValuesNew[key] == preferenceNewValue);
+
+    // Test account preference change with global preference reset
+    Manager::instance().getJamiPluginManager().resetPluginPreferencesValuesMap(installationPath_, "");
+    preferencesValuesNew = Manager::instance().getJamiPluginManager().getPluginPreferencesValuesMap(installationPath_, aliceData.accountId_);
+    preferencesValuesBobNew = Manager::instance().getJamiPluginManager().getPluginPreferencesValuesMap(installationPath_, bobData.accountId_);
+    CPPUNIT_ASSERT(preferencesValuesBobNew[key] == preferencesValuesBobOrig[key]);
+    CPPUNIT_ASSERT(preferencesValuesOrig[key] != preferencesValuesNew[key]);
+    CPPUNIT_ASSERT(preferencesValuesOrig[key] == preferencesValuesBobOrig[key]);
+    CPPUNIT_ASSERT(preferencesValuesNew[key] != preferencesValuesBobOrig[key]);
+    CPPUNIT_ASSERT(preferencesValuesNew[key] == preferenceNewValue);
+
+    // Test account preference reset
+    Manager::instance().getJamiPluginManager().resetPluginPreferencesValuesMap(installationPath_, aliceData.accountId_);
+    preferencesValuesNew = Manager::instance().getJamiPluginManager().getPluginPreferencesValuesMap(installationPath_, aliceData.accountId_);
+    preferencesValuesBobNew = Manager::instance().getJamiPluginManager().getPluginPreferencesValuesMap(installationPath_, bobData.accountId_);
+    CPPUNIT_ASSERT(preferencesValuesBobNew[key] == preferencesValuesBobOrig[key]);
+    CPPUNIT_ASSERT(preferencesValuesOrig[key] == preferencesValuesNew[key]);
+    CPPUNIT_ASSERT(preferencesValuesOrig[key] == preferencesValuesBobOrig[key]);
+    CPPUNIT_ASSERT(preferencesValuesNew[key] == preferencesValuesBobOrig[key]);
+    CPPUNIT_ASSERT(preferencesValuesNew[key] != preferenceNewValue);
+
+    // Test translations
+
+    CPPUNIT_ASSERT(!Manager::instance().getJamiPluginManager().uninstallPlugin(installationPath_));
+}
+
+
+bool
+PluginsTest::waitForSignal(CallData& callData,
+                            const std::string& expectedSignal,
+                            const std::string& expectedEvent)
+{
+    const std::chrono::seconds TIME_OUT {30};
+    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() << "\tSignal [" << sig.name_
+                        << (sig.event_.empty() ? "" : ("::" + sig.event_)) << "]";
+        }
+    }
+
+    return res;
+}
+
+void
+PluginsTest::testCall()
+{
+    Manager::instance().pluginPreferences.setPluginsEnabled(true);
+    Manager::instance().getJamiPluginManager().installPlugin(jplPath_, true);
+
+    // alice calls bob
+    // for handler available, toggle - check status - untoggle - checkstatus
+    // end call
+
+    MediaAttribute defaultAudio(MediaType::MEDIA_AUDIO);
+    defaultAudio.label_ = "audio_0";
+    defaultAudio.enabled_ = true;
+
+    MediaAttribute defaultVideo(MediaType::MEDIA_VIDEO);
+    defaultVideo.label_ = "video_0";
+    defaultVideo.enabled_ = true;
+
+    std::vector<MediaAttribute> request;
+    std::vector<MediaAttribute> answer;
+    // First offer/answer
+    request.emplace_back(MediaAttribute(defaultAudio));
+    request.emplace_back(MediaAttribute(defaultVideo));
+    answer.emplace_back(MediaAttribute(defaultAudio));
+    answer.emplace_back(MediaAttribute(defaultVideo));
+
+    JAMI_INFO("Start call between alice and Bob");
+    aliceData.callId_ = DRing::placeCallWithMedia(aliceData.accountId_, bobData.userName_, MediaAttribute::mediaAttributesToMediaMaps(request));
+    CPPUNIT_ASSERT(not aliceData.callId_.empty());
+
+    auto aliceCall = std::static_pointer_cast<SIPCall>(
+        Manager::instance().getCallFromCallID(aliceData.callId_));
+    CPPUNIT_ASSERT(aliceCall);
+
+    aliceData.callId_ = aliceCall->getCallId();
+
+    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));
+
+    // Answer the call.
+    {
+        DRing::acceptWithMedia(bobData.accountId_, bobData.callId_, MediaAttribute::mediaAttributesToMediaMaps(answer));
+    }
+
+    CPPUNIT_ASSERT_EQUAL(true,
+                         waitForSignal(bobData,
+                                       DRing::CallSignal::StateChange::name,
+                                       DRing::Call::StateEvent::CURRENT));
+
+    JAMI_INFO("BOB answered the call [%s]", bobData.callId_.c_str());
+
+    std::this_thread::sleep_for(std::chrono::seconds(3));
+    auto mediaHandlers = Manager::instance().getJamiPluginManager().getCallServicesManager().getCallMediaHandlers();
+
+    for (auto handler : mediaHandlers)
+    {
+        auto details = Manager::instance().getJamiPluginManager().getCallServicesManager().getCallMediaHandlerDetails(handler);
+        // check details expected for the test plugin
+        if(std::find(mediaHandlers_.begin(),
+                        mediaHandlers_.end(),
+                        details["name"])
+                   != mediaHandlers_.end()) {
+            Manager::instance().getJamiPluginManager().getCallServicesManager().toggleCallMediaHandler(handler, aliceData.callId_, true);
+            auto statusMap = Manager::instance().getJamiPluginManager().getCallServicesManager().getCallMediaHandlerStatus(aliceData.callId_);
+            CPPUNIT_ASSERT(std::find(statusMap.begin(), statusMap.end(), handler) != statusMap.end());
+
+            Manager::instance().getJamiPluginManager().getCallServicesManager().toggleCallMediaHandler(handler, aliceData.callId_, false);
+            statusMap = Manager::instance().getJamiPluginManager().getCallServicesManager().getCallMediaHandlerStatus(aliceData.callId_);
+            CPPUNIT_ASSERT(std::find(statusMap.begin(), statusMap.end(), handler) == statusMap.end());
+        }
+    }
+
+    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,
+                                       DRing::Call::StateEvent::HUNGUP));
+
+    JAMI_INFO("Call terminated on both sides");
+    CPPUNIT_ASSERT(!Manager::instance().getJamiPluginManager().uninstallPlugin(installationPath_));
+}
+
+void
+PluginsTest::testMessage()
+{
+    Manager::instance().pluginPreferences.setPluginsEnabled(true);
+    Manager::instance().getJamiPluginManager().installPlugin(jplPath_, true);
+
+    // alice and bob chat
+    // for handler available, toggle - check status - untoggle - checkstatus
+    // end call
+
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    auto messageBobReceived = 0, messageAliceReceived = 0;
+    bool requestReceived = false;
+    bool conversationReady = false;
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>(
+        [&](const std::string& accountId,
+            const std::string& /* conversationId */,
+            std::map<std::string, std::string> /*message*/) {
+            if (accountId == bobData.accountId_) {
+                messageBobReceived += 1;
+            } else {
+                messageAliceReceived += 1;
+            }
+            cv.notify_one();
+        }));
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>(
+            [&](const std::string& /*accountId*/,
+                const std::string& /* conversationId */,
+                std::map<std::string, std::string> /*metadatas*/) {
+                requestReceived = true;
+                cv.notify_one();
+            }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& accountId, const std::string& /* conversationId */) {
+            if (accountId == bobData.accountId_) {
+                conversationReady = true;
+                cv.notify_one();
+            }
+        }));
+    DRing::registerSignalHandlers(confHandlers);
+
+    auto convId = DRing::startConversation(aliceData.accountId_);
+
+    DRing::addConversationMember(aliceData.accountId_, convId, bobData.userName_);
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return requestReceived; }));
+
+    DRing::acceptConversationRequest(bobData.accountId_, convId);
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return conversationReady; }));
+
+    // Assert that repository exists
+    auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + bobData.accountId_
+                    + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
+    CPPUNIT_ASSERT(fileutils::isDirectory(repoPath));
+    // Wait that alice sees Bob
+    cv.wait_for(lk, 30s, [&]() { return messageAliceReceived == 2; });
+
+    auto chatHandlers = Manager::instance().getJamiPluginManager().getChatServicesManager().getChatHandlers();
+
+    for (auto handler : chatHandlers)
+    {
+        auto details = Manager::instance().getJamiPluginManager().getChatServicesManager().getChatHandlerDetails(handler);
+        // check details expected for the test plugin
+        if(std::find(chatHandlers_.begin(),
+                        chatHandlers_.end(),
+                        details["name"])
+                   != chatHandlers_.end()) {
+            Manager::instance().getJamiPluginManager().getChatServicesManager().toggleChatHandler(handler, aliceData.accountId_, convId, true);
+            auto statusMap = Manager::instance().getJamiPluginManager().getChatServicesManager().getChatHandlerStatus(aliceData.accountId_, convId);
+            CPPUNIT_ASSERT(std::find(statusMap.begin(), statusMap.end(), handler) != statusMap.end());
+
+            DRing::sendMessage(aliceData.accountId_, convId, "hi"s, "");
+            cv.wait_for(lk, 30s, [&]() { return messageBobReceived == 1; });
+
+            Manager::instance().getJamiPluginManager().getChatServicesManager().toggleChatHandler(handler, aliceData.accountId_, convId, false);
+            statusMap = Manager::instance().getJamiPluginManager().getChatServicesManager().getChatHandlerStatus(aliceData.accountId_, convId);
+            CPPUNIT_ASSERT(std::find(statusMap.begin(), statusMap.end(), handler) == statusMap.end());
+        }
+    }
+
+    CPPUNIT_ASSERT(!Manager::instance().getJamiPluginManager().uninstallPlugin(installationPath_));
+}
+
+} // namespace test
+} // namespace jami
+
+RING_TEST_RUNNER(jami::test::PluginsTest::name())