From dcacf40e82a7c0ca02817e9c8e8ae4051bf4e5ad Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Blin?=
 <sebastien.blin@savoirfairelinux.com>
Date: Thu, 17 Dec 2020 14:42:45 -0500
Subject: [PATCH] swarm: add onConversationError()

This new API gives a feedback to clients to know when an error
occurs while interacting with conversations or retrieving new
messages. For now, it's more for debugging purpose, but can
be used for admins to know if somebody is trying to do bad commits

Change-Id: I1ec797a51bdc5880d0d116b53faf5c7e13af1e8b
GitLab: #373
---
 .../cx.ring.Ring.ConfigurationManager.xml     |   27 +
 bin/dbus/dbusclient.cpp                       |    2 +
 bin/jni/conversation.i                        |    2 +
 bin/jni/jni_interface.i                       |    3 +-
 bin/nodejs/conversation.i                     |    2 +
 src/client/ring_signal.cpp                    |    1 +
 src/dring/conversation_interface.h            |    8 +
 src/jamidht/conversation.cpp                  |   40 +-
 src/jamidht/conversationrepository.cpp        |  214 ++-
 src/jamidht/conversationrepository.h          |   11 +
 src/jamidht/jamiaccount.cpp                   |  305 +++--
 test/unitTest/conversation/conversation.cpp   | 1169 +++++++++++++++--
 12 files changed, 1499 insertions(+), 285 deletions(-)

diff --git a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml
index 5e3c6f3e38..a978d06391 100644
--- a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml
+++ b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml
@@ -1956,6 +1956,33 @@
            </arg>
        </signal>
 
+       <signal name="onConversationError" tp:name-for-bindings="onConversationError">
+           <tp:added version="10.0.0"/>
+           <tp:docstring>
+               Notify clients when an error occurs in a conversation
+           </tp:docstring>
+           <arg type="s" name="account_id">
+               <tp:docstring>
+                   Account id related
+               </tp:docstring>
+           </arg>
+           <arg type="s" name="conversation_id">
+               <tp:docstring>
+                   Conversation id
+               </tp:docstring>
+           </arg>
+           <arg type="u" name="code">
+               <tp:docstring>
+                   TODO
+               </tp:docstring>
+           </arg>
+           <arg type="s" name="what">
+               <tp:docstring>
+                   The error's description
+               </tp:docstring>
+           </arg>
+       </signal>
+
        <signal name="debugMessageReceived" tp:name-for-bindings="debugMessageReceived">
            <tp:added version="5.2.0"/>
            <tp:docstring>
diff --git a/bin/dbus/dbusclient.cpp b/bin/dbus/dbusclient.cpp
index 0881232c84..6724539a51 100644
--- a/bin/dbus/dbusclient.cpp
+++ b/bin/dbus/dbusclient.cpp
@@ -305,6 +305,8 @@ DBusClient::initLibrary(int flags)
             bind(&DBusConfigurationManager::conversationRemoved, confM, _1, _2)),
         exportable_callback<ConversationSignal::ConversationMemberEvent>(
             bind(&DBusConfigurationManager::conversationMemberEvent, confM, _1, _2, _3, _4)),
+        exportable_callback<ConversationSignal::OnConversationError>(
+            bind(&DBusConfigurationManager::onConversationError, confM, _1, _2, _3, _4)),
     };
 
 #ifdef ENABLE_VIDEO
diff --git a/bin/jni/conversation.i b/bin/jni/conversation.i
index 40b249a44d..1427bd08ad 100644
--- a/bin/jni/conversation.i
+++ b/bin/jni/conversation.i
@@ -31,6 +31,7 @@ public:
     virtual void conversationReady(const std::string& /*accountId*/, const std::string& /* conversationId */){}
     virtual void conversationRemoved(const std::string& /*accountId*/, const std::string& /* conversationId */){}
     virtual void conversationMemberEvent(const std::string& /*accountId*/, const std::string& /* conversationId */, const std::string& /* memberUri */, int /* event */){}
+    virtual void onConversationError(const std::string& /*accountId*/, const std::string& /* conversationId */, uint32_t /* code */, const std::string& /* what */){}
 };
 %}
 
@@ -66,4 +67,5 @@ public:
     virtual void conversationReady(const std::string& /*accountId*/, const std::string& /* conversationId */){}
     virtual void conversationRemoved(const std::string& /*accountId*/, const std::string& /* conversationId */){}
     virtual void conversationMemberEvent(const std::string& /*accountId*/, const std::string& /* conversationId */, const std::string& /* memberUri */, int /* event */){}
+    virtual void onConversationError(const std::string& /*accountId*/, const std::string& /* conversationId */, uint32_t /* code */, const std::string& /* what */){}
 };
\ No newline at end of file
diff --git a/bin/jni/jni_interface.i b/bin/jni/jni_interface.i
index 34030f3425..da3e951268 100644
--- a/bin/jni/jni_interface.i
+++ b/bin/jni/jni_interface.i
@@ -320,7 +320,8 @@ void init(ConfigurationCallback* confM, Callback* callM, PresenceCallback* presM
         exportable_callback<ConversationSignal::ConversationRequestReceived>(bind(&ConversationCallback::conversationRequestReceived, convM, _1, _2, _3)),
         exportable_callback<ConversationSignal::ConversationReady>(bind(&ConversationCallback::conversationReady, convM, _1, _2)),
         exportable_callback<ConversationSignal::ConversationRemoved>(bind(&ConversationCallback::conversationRemoved, convM, _1, _2)),
-        exportable_callback<ConversationSignal::ConversationMemberEvent>(bind(&ConversationCallback::conversationMemberEvent, convM, _1, _2, _3, _4))
+        exportable_callback<ConversationSignal::ConversationMemberEvent>(bind(&ConversationCallback::conversationMemberEvent, convM, _1, _2, _3, _4)),
+        exportable_callback<ConversationSignal::OnConversationError>(bind(&ConversationCallback::onConversationError, convM, _1, _2, _3, _4))
     };
 
     if (!DRing::init(static_cast<DRing::InitFlag>(DRing::DRING_FLAG_DEBUG)))
diff --git a/bin/nodejs/conversation.i b/bin/nodejs/conversation.i
index 04377ee7fe..af662e05ec 100644
--- a/bin/nodejs/conversation.i
+++ b/bin/nodejs/conversation.i
@@ -31,6 +31,7 @@ public:
     virtual void conversationReady(const std::string& /*accountId*/, const std::string& /* conversationId */){}
     virtual void conversationRemoved(const std::string& /*accountId*/, const std::string& /* conversationId */){}
     virtual void conversationMemberEvent(const std::string& /*accountId*/, const std::string& /* conversationId */, const std::string& /* memberUri */, int /* event */){}
+    virtual void onConversationError(const std::string& /*accountId*/, const std::string& /* conversationId */, uint32_t /* code */, const std::string& /* what */){}
 };
 %}
 
@@ -79,4 +80,5 @@ public:
     virtual void conversationReady(const std::string& /*accountId*/, const std::string& /* conversationId */){}
     virtual void conversationRemoved(const std::string& /*accountId*/, const std::string& /* conversationId */){}
     virtual void conversationMemberEvent(const std::string& /*accountId*/, const std::string& /* conversationId */, const std::string& /* memberUri */, int /* event */){}
+    virtual void onConversationError(const std::string& /*accountId*/, const std::string& /* conversationId */, uint32_t /* code */, const std::string& /* what */){}
 };
\ No newline at end of file
diff --git a/src/client/ring_signal.cpp b/src/client/ring_signal.cpp
index fe6d35cd5a..4397e6d482 100644
--- a/src/client/ring_signal.cpp
+++ b/src/client/ring_signal.cpp
@@ -132,6 +132,7 @@ getSignalHandlers()
         exported_callback<DRing::ConversationSignal::ConversationReady>(),
         exported_callback<DRing::ConversationSignal::ConversationRemoved>(),
         exported_callback<DRing::ConversationSignal::ConversationMemberEvent>(),
+        exported_callback<DRing::ConversationSignal::OnConversationError>(),
     };
 
     return handlers;
diff --git a/src/dring/conversation_interface.h b/src/dring/conversation_interface.h
index 5bda2bed21..9e33a649c2 100644
--- a/src/dring/conversation_interface.h
+++ b/src/dring/conversation_interface.h
@@ -107,6 +107,14 @@ struct DRING_PUBLIC ConversationSignal
                              const std::string& /* memberUri */,
                              int /* event 0 = add, 1 = joins, 2 = leave, 3 = banned */);
     };
+    struct DRING_PUBLIC OnConversationError
+    {
+        constexpr static const char* name = "OnConversationError";
+        using cb_type = void(const std::string& /*accountId*/,
+                             const std::string& /* conversationId */,
+                             int code,
+                             const std::string& what);
+    };
 };
 
 } // namespace DRing
diff --git a/src/jamidht/conversation.cpp b/src/jamidht/conversation.cpp
index dae2f8a50f..2acce64794 100644
--- a/src/jamidht/conversation.cpp
+++ b/src/jamidht/conversation.cpp
@@ -63,6 +63,10 @@ public:
                                                                 remoteDevice,
                                                                 conversationId);
         if (!repository_) {
+            if (auto shared = account.lock()) {
+                emitSignal<DRing::ConversationSignal::OnConversationError>(
+                    shared->getAccountID(), conversationId, EFETCH, "Couldn't clone repository");
+            }
             throw std::logic_error("Couldn't clone repository");
         }
     }
@@ -97,12 +101,7 @@ Conversation::Impl::isAdmin() const
 
     auto adminsPath = repoPath() + DIR_SEPARATOR_STR + "admins";
     auto cert = shared->identity().second;
-    auto parentCert = cert->issuer;
-    if (!parentCert) {
-        JAMI_ERR("Parent cert is null!");
-        return false;
-    }
-    auto uri = parentCert->getId().toString();
+    auto uri = cert->getIssuerUID();
     return fileutils::isFile(fileutils::getFullPath(adminsPath, uri + ".crt"));
 }
 
@@ -123,10 +122,24 @@ Conversation::Impl::convCommitToMap(const std::vector<ConversationCommit>& commi
     for (const auto& commit : commits) {
         auto authorDevice = commit.author.email;
         auto cert = tls::CertificateStore::instance().getCertificate(authorDevice);
-        if (!cert && cert->issuer) {
-            JAMI_WARN("No author found for commit %s", commit.id.c_str());
+        if (!cert) {
+            JAMI_WARN("No author found for commit %s, reload certificates", commit.id.c_str());
+            if (repository_)
+                repository_->pinCertificates();
+            // Get certificate from repo
+            try {
+                auto certPath = fileutils::getFullPath(repoPath(), std::string("devices") + DIR_SEPARATOR_STR + authorDevice + ".crt");
+                auto deviceCert = fileutils::loadTextFile(certPath);
+                cert = std::make_shared<crypto::Certificate>(deviceCert);
+                if (!cert) {
+                    JAMI_ERR("No author found for commit %s", commit.id.c_str());
+                    continue;
+                }
+            } catch (...) {
+                continue;
+            }
         }
-        auto authorId = cert->issuer->getId().toString();
+        auto authorId = cert->getIssuerUID();
         std::string parents;
         auto parentsSize = commit.parents.size();
         for (std::size_t i = 0; i < parentsSize; ++i) {
@@ -522,15 +535,6 @@ Conversation::generateInvitation() const
     std::map<std::string, std::string> invite;
     Json::Value root;
     root["conversationId"] = id();
-    // TODO remove, cause the peer cannot trust?
-    // Or add signatures?
-    for (const auto& member : getMembers()) {
-        Json::Value jsonMember;
-        for (const auto& [key, value] : member) {
-            jsonMember[key] = value;
-        }
-        root["members"].append(jsonMember);
-    }
     // TODO metadatas
     Json::StreamWriterBuilder wbuilder;
     wbuilder["commentStyle"] = "None";
diff --git a/src/jamidht/conversationrepository.cpp b/src/jamidht/conversationrepository.cpp
index 94abeebc22..693dd27aa0 100644
--- a/src/jamidht/conversationrepository.cpp
+++ b/src/jamidht/conversationrepository.cpp
@@ -23,6 +23,7 @@
 #include "fileutils.h"
 #include "gittransport.h"
 #include "string_utils.h"
+#include "client/ring_signal.h"
 
 using random_device = dht::crypto::random_device;
 
@@ -123,7 +124,8 @@ public:
     mutable std::mutex membersMtx_ {};
     std::vector<ConversationMember> members_ {};
 
-    std::vector<ConversationMember> members() const {
+    std::vector<ConversationMember> members() const
+    {
         std::lock_guard<std::mutex> lk(membersMtx_);
         return members_;
     }
@@ -427,7 +429,8 @@ ConversationRepository::Impl::createMergeCommit(git_index* index, const std::str
         return false;
     }
     GitAnnotatedCommit annotated {annotated_ptr, git_annotated_commit_free};
-    if (git_commit_lookup(&parent, repository_.get(), git_annotated_commit_id(annotated.get())) < 0) {
+    if (git_commit_lookup(&parent, repository_.get(), git_annotated_commit_id(annotated.get()))
+        < 0) {
         JAMI_ERR("Couldn't lookup commit %s", wanted_ref.c_str());
         return false;
     }
@@ -485,7 +488,12 @@ ConversationRepository::Impl::createMergeCommit(git_index* index, const std::str
         JAMI_INFO("New merge commit added with id: %s", commit_str);
         // Move commit to main branch
         git_reference* ref_ptr = nullptr;
-        if (git_reference_create(&ref_ptr, repository_.get(), "refs/heads/main", &commit_oid, true, nullptr)
+        if (git_reference_create(&ref_ptr,
+                                 repository_.get(),
+                                 "refs/heads/main",
+                                 &commit_oid,
+                                 true,
+                                 nullptr)
             < 0) {
             JAMI_WARN("Could not move commit to main");
         }
@@ -517,7 +525,12 @@ ConversationRepository::Impl::mergeFastforward(const git_oid* target_oid, int is
         const auto* symbolic_ref = git_reference_symbolic_target(head_ref.get());
 
         // Create our main reference on the target OID
-        if (git_reference_create(&target_ref_ptr, repository_.get(), symbolic_ref, target_oid, 0, nullptr)
+        if (git_reference_create(&target_ref_ptr,
+                                 repository_.get(),
+                                 symbolic_ref,
+                                 target_oid,
+                                 0,
+                                 nullptr)
             < 0) {
             JAMI_ERR("failed to create main reference");
             return false;
@@ -655,9 +668,9 @@ ConversationRepository::Impl::checkVote(const std::string& userDevice,
     }
 
     auto cert = tls::CertificateStore::instance().getCertificate(userDevice);
-    if (!cert && cert->issuer)
+    if (!cert)
         return false;
-    auto userUri = cert->issuer->getId().toString();
+    auto userUri = cert->getIssuerUID();
     // Check that voter is admin
     auto adminFile = std::string("admins") + DIR_SEPARATOR_STR + userUri + ".crt";
 
@@ -740,9 +753,9 @@ ConversationRepository::Impl::checkValidAdd(const std::string& userDevice,
                                             const std::string& parentId) const
 {
     auto cert = tls::CertificateStore::instance().getCertificate(userDevice);
-    if (!cert && cert->issuer)
+    if (!cert)
         return false;
-    auto userUri = cert->issuer->getId().toString();
+    auto userUri = cert->getIssuerUID();
 
     auto repo = repository();
     std::string repoPath = git_repository_workdir(repository_.get());
@@ -825,9 +838,9 @@ ConversationRepository::Impl::checkValidJoins(const std::string& userDevice,
                                               const std::string& parentId) const
 {
     auto cert = tls::CertificateStore::instance().getCertificate(userDevice);
-    if (!cert && cert->issuer)
+    if (!cert)
         return false;
-    auto userUri = cert->issuer->getId().toString();
+    auto userUri = cert->getIssuerUID();
     // Check no other files changed
     auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId));
     auto oneone = mode() == ConversationMode::ONE_TO_ONE;
@@ -888,9 +901,9 @@ ConversationRepository::Impl::checkValidRemove(const std::string& userDevice,
                                                const std::string& parentId) const
 {
     auto cert = tls::CertificateStore::instance().getCertificate(userDevice);
-    if (!cert && cert->issuer)
+    if (!cert)
         return false;
-    auto userUri = cert->issuer->getId().toString();
+    auto userUri = cert->getIssuerUID();
     auto removeSelf = userUri == uriMember;
 
     // Retrieve tree for recent commit
@@ -918,7 +931,7 @@ ConversationRepository::Impl::checkValidRemove(const std::string& userDevice,
             // Ignore
         } else if (std::regex_match(f, base_match, regex_votes)) {
             if (base_match.size() != 4 or base_match[2] != uriMember) {
-                JAMI_ERR("Invalid vote file detected :%s", f.c_str());
+                JAMI_ERR("Invalid vote file detected: %s", f.c_str());
                 return false;
             }
             voters.emplace_back(base_match[3]);
@@ -950,9 +963,9 @@ ConversationRepository::Impl::checkValidRemove(const std::string& userDevice,
             return false;
         }
         cert = tls::CertificateStore::instance().getCertificate(deviceUri);
-        if (!cert && cert->issuer)
+        if (!cert)
             return false;
-        if (uriMember != cert->issuer->getId().toString()
+        if (uriMember != cert->getIssuerUID()
             and uriMember != deviceUri /* If device is removed */) {
             JAMI_ERR("device removed but not for removed user (%s)", deviceFile.c_str());
             return false;
@@ -1001,9 +1014,9 @@ ConversationRepository::Impl::isValidUserAtCommit(const std::string& userDevice,
                                                   const std::string& commitId) const
 {
     auto cert = tls::CertificateStore::instance().getCertificate(userDevice);
-    if (!cert && cert->issuer)
+    if (!cert || !cert->issuer)
         return false;
-    auto userUri = cert->issuer->getId().toString();
+    auto userUri = cert->getIssuerUID();
 
     // Retrieve tree for commit
     auto tree = treeAtCommit(commitId);
@@ -1045,9 +1058,11 @@ ConversationRepository::Impl::checkInitialCommit(const std::string& userDevice,
                                                  const std::string& commitId) const
 {
     auto cert = tls::CertificateStore::instance().getCertificate(userDevice);
-    if (!cert && cert->issuer)
+    if (!cert) {
+        JAMI_ERR("Cannot find certificate for %s", userDevice.c_str());
         return false;
-    auto userUri = cert->issuer->getId().toString();
+    }
+    auto userUri = cert->getIssuerUID();
     auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, ""));
 
     try {
@@ -1191,6 +1206,12 @@ ConversationRepository::Impl::mode() const
     auto lastMsg = log(id_, "", 1);
     if (lastMsg.size() == 0) {
         throw std::logic_error("Can't retrieve first commit");
+        if (auto shared = account_.lock()) {
+            emitSignal<DRing::ConversationSignal::OnConversationError>(shared->getAccountID(),
+                                                                       id_,
+                                                                       EINVALIDMODE,
+                                                                       "No initial commit");
+        }
     }
     auto commitMsg = lastMsg[0].commit_msg;
 
@@ -1200,9 +1221,21 @@ ConversationRepository::Impl::mode() const
     auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
     if (!reader->parse(commitMsg.data(), commitMsg.data() + commitMsg.size(), &root, &err)) {
         throw std::logic_error("Can't retrieve first commit");
+        if (auto shared = account_.lock()) {
+            emitSignal<DRing::ConversationSignal::OnConversationError>(shared->getAccountID(),
+                                                                       id_,
+                                                                       EINVALIDMODE,
+                                                                       "No initial commit");
+        }
     }
     if (!root.isMember("mode")) {
         throw std::logic_error("No mode detected for initial commit");
+        if (auto shared = account_.lock()) {
+            emitSignal<DRing::ConversationSignal::OnConversationError>(shared->getAccountID(),
+                                                                       id_,
+                                                                       EINVALIDMODE,
+                                                                       "No mode detected");
+        }
     }
     int mode = root["mode"].asInt();
 
@@ -1220,6 +1253,12 @@ ConversationRepository::Impl::mode() const
         mode_ = ConversationMode::PUBLIC;
         break;
     default:
+        if (auto shared = account_.lock()) {
+            emitSignal<DRing::ConversationSignal::OnConversationError>(shared->getAccountID(),
+                                                                       id_,
+                                                                       EINVALIDMODE,
+                                                                       "Incorrect mode detected");
+        }
         throw std::logic_error("Incorrect mode detected");
         break;
     }
@@ -1394,7 +1433,11 @@ ConversationRepository::Impl::log(const std::string& from, const std::string& to
         cc->author = std::move(author);
         cc->parents = std::move(parents);
         git_buf signature = {}, signed_data = {};
-        if (git_commit_extract_signature(&signature, &signed_data, repository_.get(), &oid, "signature")
+        if (git_commit_extract_signature(&signature,
+                                         &signed_data,
+                                         repository_.get(),
+                                         &oid,
+                                         "signature")
             < 0) {
             JAMI_WARN("Could not extract signature for commit %s", id.c_str());
         } else {
@@ -1467,12 +1510,12 @@ ConversationRepository::Impl::getInitialMembers() const
         return {};
     }
     auto commit = firstCommit[0];
+
     auto authorDevice = commit.author.email;
     auto cert = tls::CertificateStore::instance().getCertificate(authorDevice);
-    if (!cert && cert->issuer) {
+    if (!cert)
         return {};
-    }
-    auto authorId = cert->issuer->getId().toString();
+    auto authorId = cert->getIssuerUID();
     if (mode() == ConversationMode::ONE_TO_ONE) {
         std::string err;
         Json::Value root;
@@ -1500,12 +1543,11 @@ ConversationRepository::Impl::initMembers()
     std::lock_guard<std::mutex> lk(membersMtx_);
     members_.clear();
     std::string repoPath = git_repository_workdir(repository_.get());
-    std::vector<std::string> paths = {
-        repoPath + DIR_SEPARATOR_STR + "invited",
-        repoPath + DIR_SEPARATOR_STR + "admins",
-        repoPath + DIR_SEPARATOR_STR + "members",
-        repoPath + DIR_SEPARATOR_STR + "banned" + DIR_SEPARATOR_STR + "members"
-    };
+    std::vector<std::string> paths = {repoPath + DIR_SEPARATOR_STR + "invited",
+                                      repoPath + DIR_SEPARATOR_STR + "admins",
+                                      repoPath + DIR_SEPARATOR_STR + "members",
+                                      repoPath + DIR_SEPARATOR_STR + "banned" + DIR_SEPARATOR_STR
+                                          + "members"};
     std::vector<MemberRole> roles = {
         MemberRole::INVITED,
         MemberRole::ADMIN,
@@ -1514,7 +1556,7 @@ ConversationRepository::Impl::initMembers()
     };
 
     auto i = 0;
-    for (const auto& p: paths) {
+    for (const auto& p : paths) {
         for (const auto& f : fileutils::readDirectory(p)) {
             auto pos = f.find(".crt");
             auto uri = f.substr(0, pos);
@@ -1650,6 +1692,7 @@ ConversationRepository::cloneConversation(const std::weak_ptr<JamiAccount>& acco
     }
     git_repository_free(rep);
     auto repo = std::make_unique<ConversationRepository>(account, conversationId);
+    repo->pinCertificates(); // need to load certificates to validate non known members
     if (!repo->validClone()) {
         JAMI_ERR("Error when validating remote conversation");
         return nullptr;
@@ -1670,6 +1713,10 @@ ConversationRepository::Impl::validCommits(
                 JAMI_WARN("Malformed initial commit %s. Please check you use the latest "
                           "version of Jami, or that your contact is not doing unwanted stuff.",
                           commit.id.c_str());
+                if (auto shared = account_.lock()) {
+                    emitSignal<DRing::ConversationSignal::OnConversationError>(
+                        shared->getAccountID(), id_, EVALIDFETCH, "Malformed initial commit");
+                }
                 return false;
             }
         } else if (commit.parents.size() == 1) {
@@ -1680,6 +1727,10 @@ ConversationRepository::Impl::validCommits(
                     JAMI_WARN("Malformed vote commit %s. Please check you use the latest version "
                               "of Jami, or that your contact is not doing unwanted stuff.",
                               commit.id.c_str());
+                    if (auto shared = account_.lock()) {
+                        emitSignal<DRing::ConversationSignal::OnConversationError>(
+                            shared->getAccountID(), id_, EVALIDFETCH, "Malformed vote");
+                    }
                     return false;
                 }
             } else if (type == "member") {
@@ -1692,6 +1743,10 @@ ConversationRepository::Impl::validCommits(
                                    &root,
                                    &err)) {
                     JAMI_ERR() << "Failed to parse " << err;
+                    if (auto shared = account_.lock()) {
+                        emitSignal<DRing::ConversationSignal::OnConversationError>(
+                            shared->getAccountID(), id_, EVALIDFETCH, "Malformed member commit");
+                    }
                     return false;
                 }
                 std::string action = root["action"].asString();
@@ -1702,6 +1757,13 @@ ConversationRepository::Impl::validCommits(
                             "Malformed add commit %s. Please check you use the latest version "
                             "of Jami, or that your contact is not doing unwanted stuff.",
                             commit.id.c_str());
+                        if (auto shared = account_.lock()) {
+                            emitSignal<DRing::ConversationSignal::OnConversationError>(
+                                shared->getAccountID(),
+                                id_,
+                                EVALIDFETCH,
+                                "Malformed add member commit");
+                        }
                         return false;
                     }
                 } else if (action == "join") {
@@ -1710,6 +1772,13 @@ ConversationRepository::Impl::validCommits(
                             "Malformed joins commit %s. Please check you use the latest version "
                             "of Jami, or that your contact is not doing unwanted stuff.",
                             commit.id.c_str());
+                        if (auto shared = account_.lock()) {
+                            emitSignal<DRing::ConversationSignal::OnConversationError>(
+                                shared->getAccountID(),
+                                id_,
+                                EVALIDFETCH,
+                                "Malformed join member commit");
+                        }
                         return false;
                     }
                 } else if (action == "remove") {
@@ -1721,6 +1790,13 @@ ConversationRepository::Impl::validCommits(
                             "Malformed removes commit %s. Please check you use the latest version "
                             "of Jami, or that your contact is not doing unwanted stuff.",
                             commit.id.c_str());
+                        if (auto shared = account_.lock()) {
+                            emitSignal<DRing::ConversationSignal::OnConversationError>(
+                                shared->getAccountID(),
+                                id_,
+                                EVALIDFETCH,
+                                "Malformed remove member commit");
+                        }
                         return false;
                     }
                 } else if (action == "ban") {
@@ -1730,6 +1806,13 @@ ConversationRepository::Impl::validCommits(
                             "Malformed removes commit %s. Please check you use the latest version "
                             "of Jami, or that your contact is not doing unwanted stuff.",
                             commit.id.c_str());
+                        if (auto shared = account_.lock()) {
+                            emitSignal<DRing::ConversationSignal::OnConversationError>(
+                                shared->getAccountID(),
+                                id_,
+                                EVALIDFETCH,
+                                "Malformed ban member commit");
+                        }
                         return false;
                     }
                 } else {
@@ -1738,6 +1821,10 @@ ConversationRepository::Impl::validCommits(
                               "version of Jami, or that your contact is not doing unwanted stuff.",
                               commit.id.c_str(),
                               action.c_str());
+                    if (auto shared = account_.lock()) {
+                        emitSignal<DRing::ConversationSignal::OnConversationError>(
+                            shared->getAccountID(), id_, EVALIDFETCH, "Malformed member commit");
+                    }
                     return false;
                 }
             } else {
@@ -1749,6 +1836,10 @@ ConversationRepository::Impl::validCommits(
                               "version of Jami, or that your contact is not doing unwanted stuff.",
                               type.c_str(),
                               commit.id.c_str());
+                    if (auto shared = account_.lock()) {
+                        emitSignal<DRing::ConversationSignal::OnConversationError>(
+                            shared->getAccountID(), id_, EVALIDFETCH, "Malformed commit");
+                    }
                     return false;
                 }
             }
@@ -1762,6 +1853,12 @@ ConversationRepository::Impl::validCommits(
                     "that your contact is not doing unwanted stuff. %s",
                     validUserAtCommit.c_str(),
                     commit.commit_msg.c_str());
+                if (auto shared = account_.lock()) {
+                    emitSignal<DRing::ConversationSignal::OnConversationError>(shared->getAccountID(),
+                                                                               id_,
+                                                                               EVALIDFETCH,
+                                                                               "Malformed commit");
+                }
                 return false;
             }
         } else {
@@ -1874,7 +1971,12 @@ ConversationRepository::amend(const std::string& id, const std::string& msg)
 
     // Move commit to main branch
     git_reference* ref_ptr = nullptr;
-    if (git_reference_create(&ref_ptr, pimpl_->repository_.get(), "refs/heads/main", &commit_id, true, nullptr)
+    if (git_reference_create(&ref_ptr,
+                             pimpl_->repository_.get(),
+                             "refs/heads/main",
+                             &commit_id,
+                             true,
+                             nullptr)
         < 0) {
         JAMI_WARN("Could not move commit to main");
     }
@@ -1910,7 +2012,10 @@ ConversationRepository::fetch(const std::string& remoteDeviceId)
             JAMI_ERR("Couldn't lookup for remote %s", remoteDeviceId.c_str());
             return false;
         }
-        if (git_remote_create(&remote_ptr, pimpl_->repository_.get(), remoteDeviceId.c_str(), channelName.c_str())
+        if (git_remote_create(&remote_ptr,
+                              pimpl_->repository_.get(),
+                              remoteDeviceId.c_str(),
+                              channelName.c_str())
             < 0) {
             JAMI_ERR("Could not create remote for repository for conversation %s",
                      pimpl_->id_.c_str());
@@ -1936,6 +2041,13 @@ ConversationRepository::fetch(const std::string& remoteDeviceId)
             JAMI_ERR("Could not fetch remote repository for conversation %s: %s",
                      pimpl_->id_.c_str(),
                      err->message);
+
+            if (auto shared = pimpl_->account_.lock()) {
+                emitSignal<DRing::ConversationSignal::OnConversationError>(shared->getAccountID(),
+                                                                           pimpl_->id_,
+                                                                           EFETCH,
+                                                                           err->message);
+            }
         }
         return false;
     }
@@ -2054,7 +2166,8 @@ ConversationRepository::merge(const std::string& merge_id)
     git_merge_analysis_t analysis;
     git_merge_preference_t preference;
     const git_annotated_commit* const_annotated = annotated.get();
-    if (git_merge_analysis(&analysis, &preference, pimpl_->repository_.get(), &const_annotated, 1) < 0) {
+    if (git_merge_analysis(&analysis, &preference, pimpl_->repository_.get(), &const_annotated, 1)
+        < 0) {
         JAMI_ERR("Merge operation aborted: repository analysis failed");
         return false;
     }
@@ -2091,7 +2204,8 @@ ConversationRepository::merge(const std::string& merge_id)
             return false;
         }
 
-        if (git_merge(pimpl_->repository_.get(), &const_annotated, 1, &merge_opts, &checkout_opts) < 0) {
+        if (git_merge(pimpl_->repository_.get(), &const_annotated, 1, &merge_opts, &checkout_opts)
+            < 0) {
             const git_error* err = giterr_last();
             if (err)
                 JAMI_ERR("Git merge failed: %s", err->message);
@@ -2199,7 +2313,6 @@ ConversationRepository::join()
             pimpl_->members_.emplace_back(ConversationMember {uri, MemberRole::MEMBER});
     }
 
-
     return commitMessage(Json::writeString(wbuilder, json));
 }
 
@@ -2274,7 +2387,9 @@ ConversationRepository::leave()
 
     {
         std::lock_guard<std::mutex> lk(pimpl_->membersMtx_);
-        std::remove_if(pimpl_->members_.begin(), pimpl_->members_.end(), [&](auto& member) { return member.uri == account->getUsername(); });
+        std::remove_if(pimpl_->members_.begin(), pimpl_->members_.end(), [&](auto& member) {
+            return member.uri == account->getUsername();
+        });
     }
 
     return commitMessage(Json::writeString(wbuilder, json));
@@ -2306,12 +2421,7 @@ ConversationRepository::voteKick(const std::string& uri, bool isDevice)
     if (!account)
         return {};
     auto cert = account->identity().second;
-    auto parentCert = cert->issuer;
-    if (!parentCert) {
-        JAMI_ERR("Parent cert is null!");
-        return {};
-    }
-    auto adminUri = parentCert->getId().toString();
+    auto adminUri = cert->getIssuerUID();
     if (adminUri == uri) {
         JAMI_WARN("Admin tried to ban theirself");
         return {};
@@ -2491,4 +2601,24 @@ ConversationRepository::refreshMembers() const
     return pimpl_->initMembers();
 }
 
+void
+ConversationRepository::pinCertificates()
+{
+    auto repo = pimpl_->repository();
+    if (!repo)
+        return;
+
+    std::string repoPath = git_repository_workdir(repo.get());
+    std::vector<std::string> paths = {repoPath + "admins",
+                                      repoPath + "members",
+                                      repoPath + "devices"};
+
+    for (const auto& path : paths) {
+        tls::CertificateStore::instance().pinCertificatePath(path, [](auto& ids) {
+            for (const auto& id : ids)
+                JAMI_ERR("@@@ LOADED %s", id.c_str());
+        });
+    }
+}
+
 } // namespace jami
diff --git a/src/jamidht/conversationrepository.h b/src/jamidht/conversationrepository.h
index b4a104f199..f6c2752c5a 100644
--- a/src/jamidht/conversationrepository.h
+++ b/src/jamidht/conversationrepository.h
@@ -45,6 +45,10 @@ using GitIndexConflictIterator
 
 namespace jami {
 
+constexpr auto EFETCH = 1;
+constexpr auto EINVALIDMODE = 2;
+constexpr auto EVALIDFETCH = 3;
+
 class JamiAccount;
 class ChannelSocket;
 
@@ -255,6 +259,13 @@ public:
      */
     void refreshMembers() const;
 
+
+    /**
+     * Because conversations can contains non contacts certificates, this methods
+     * loads certificates in conversations into the cert store
+     */
+    void pinCertificates();
+
 private:
     ConversationRepository() = delete;
     class Impl;
diff --git a/src/jamidht/jamiaccount.cpp b/src/jamidht/jamiaccount.cpp
index 9b56f40434..abf36690e9 100644
--- a/src/jamidht/jamiaccount.cpp
+++ b/src/jamidht/jamiaccount.cpp
@@ -1215,34 +1215,29 @@ JamiAccount::loadAccount(const std::string& archive_password,
                const std::string& conversationId,
                const std::vector<uint8_t>& payload,
                time_t received) {
-            dht::ThreadPool::computation().run([w = weak(),
-                                                uri,
-                                                payload = std::move(payload),
-                                                received,
-                                                conversationId] {
-                if (auto acc = w.lock()) {
-                    if (!conversationId.empty()) {
-                        std::lock_guard<std::mutex> lk(acc->conversationsRequestsMtx_);
-                        auto it = acc->conversationsRequests_.find(conversationId);
-                        if (it != acc->conversationsRequests_.end()) {
-                            JAMI_INFO(
-                                "[Account %s] Received a request for a conversation already existing. "
-                                "Ignore",
-                                acc->getAccountID().c_str());
-                            return;
+            dht::ThreadPool::computation().run(
+                [w = weak(), uri, payload = std::move(payload), received, conversationId] {
+                    if (auto acc = w.lock()) {
+                        if (!conversationId.empty()) {
+                            std::lock_guard<std::mutex> lk(acc->conversationsRequestsMtx_);
+                            auto it = acc->conversationsRequests_.find(conversationId);
+                            if (it != acc->conversationsRequests_.end()) {
+                                JAMI_INFO("[Account %s] Received a request for a conversation "
+                                          "already existing. "
+                                          "Ignore",
+                                          acc->getAccountID().c_str());
+                                return;
+                            }
+                            ConversationRequest req;
+                            req.from = uri;
+                            req.conversationId = conversationId;
+                            req.received = std::time(nullptr);
+                            acc->conversationsRequests_[conversationId] = std::move(req);
                         }
-                        ConversationRequest req;
-                        req.from = uri;
-                        req.conversationId = conversationId;
-                        req.received = std::time(nullptr);
-                        acc->conversationsRequests_[conversationId] = std::move(req);
+                        emitSignal<DRing::ConfigurationSignal::IncomingTrustRequest>(
+                            acc->getAccountID(), uri, payload, received);
                     }
-                    emitSignal<DRing::ConfigurationSignal::IncomingTrustRequest>(acc->getAccountID(),
-                                                                                 uri,
-                                                                                 payload,
-                                                                                 received);
-                }
-            });
+                });
         },
         [this](const std::map<dht::InfoHash, KnownDevice>& devices) {
             std::map<std::string, std::string> ids;
@@ -2209,11 +2204,15 @@ JamiAccount::onTrackedBuddyOnline(const dht::InfoHash& contactId)
         auto convId = getOneToOneConversation(id);
         if (convId.empty())
             return;
-        // In this case, the TrustRequest was sent but never confirmed (cause the contact was offline maybe)
-        // To avoid the contact to never receive the conv request, retry there
+        // In this case, the TrustRequest was sent but never confirmed (cause the contact was
+        // offline maybe) To avoid the contact to never receive the conv request, retry there
         std::lock_guard<std::mutex> lock(configurationMutex_);
         if (accountManager_)
-            accountManager_->sendTrustRequest(id, convId, {}); /* TODO payload?, MessageEngine not generic and will be able to move to conversation's requests */
+            accountManager_
+                ->sendTrustRequest(id,
+                                   convId,
+                                   {}); /* TODO payload?, MessageEngine not generic and will be able
+                                           to move to conversation's requests */
     }
 }
 
@@ -2469,11 +2468,11 @@ JamiAccount::doRegister_()
                     ->findCertificate(deviceId,
                                       [this, &accept](
                                           const std::shared_ptr<dht::crypto::Certificate>& cert) {
-                                          if (not cert or not cert->issuer) {
+                                          if (not cert) {
                                               accept.set_value(false);
                                               return;
                                           }
-                                          accept.set_value(cert->issuer->getId().toString()
+                                          accept.set_value(cert->getIssuerUID()
                                                            == accountManager_->getInfo()->accountId);
                                       });
                 fut.wait();
@@ -2549,21 +2548,20 @@ JamiAccount::doRegister_()
                         auto conversation = conversations_.find(conversationId);
                         if (conversation == conversations_.end()) {
                             JAMI_WARN("[Account %s] Git server requested, but for a non existing "
-                                    "conversation (%s)",
-                                    getAccountID().c_str(),
-                                    conversationId.c_str());
+                                      "conversation (%s)",
+                                      getAccountID().c_str(),
+                                      conversationId.c_str());
                             return;
                         }
                         if (conversation->second->isBanned(remoteDevice)) {
                             JAMI_WARN("[Account %s] %s is a banned device in conversation %s",
-                                    getAccountID().c_str(),
-                                    remoteDevice.c_str(),
-                                    conversationId.c_str());
+                                      getAccountID().c_str(),
+                                      remoteDevice.c_str(),
+                                      conversationId.c_str());
                             return;
                         }
                     }
 
-
                     if (gitSocket(deviceId.toString(), conversationId) == channel) {
                         // The onConnectionReady is already used as client (for retrieving messages)
                         // So it's not the server socket
@@ -3312,7 +3310,8 @@ JamiAccount::removeContact(const std::string& uri, bool ban)
                 if (conv->mode() == ConversationMode::ONE_TO_ONE) {
                     auto initMembers = conv->getInitialMembers();
                     if ((isSelf && initMembers.size() == 1)
-                        || std::find(initMembers.begin(), initMembers.end(), uri) != initMembers.end())
+                        || std::find(initMembers.begin(), initMembers.end(), uri)
+                               != initMembers.end())
                         toRm.emplace_back(key);
                 }
             } catch (const std::exception& e) {
@@ -3738,8 +3737,8 @@ JamiAccount::requestPeerConnection(
                                          isVCard,
                                          channeledConnectedCb,
                                          onChanneledCancelled);
-    //auto convId = getOneToOneConversation(info.peer);
-    //if (!convId.empty()) {
+    // auto convId = getOneToOneConversation(info.peer);
+    // if (!convId.empty()) {
     //    Json::Value value;
     //    value["tid"] = std::to_string(tid);
     //    value["type"] = "application/data-transfer+json";
@@ -3938,7 +3937,6 @@ JamiAccount::acceptConversationRequest(const std::string& conversationId)
                         lk.unlock();
                         // Save the git socket
                         addGitSocket(dev.toString(), request.conversationId, socket);
-                        // TODO when do we remove the gitSocket?
                     } else {
                         lk.unlock();
                         socket->shutdown();
@@ -3979,8 +3977,8 @@ JamiAccount::handlePendingConversations()
     for (auto it = pendingConversationsFetch_.begin(); it != pendingConversationsFetch_.end();) {
         if (it->second.ready) {
             // Clone and store conversation
+            auto conversationId = it->first;
             try {
-                auto conversationId = it->first;
                 auto conversation = std::make_shared<Conversation>(weak(),
                                                                    it->second.deviceId,
                                                                    conversationId);
@@ -4011,6 +4009,10 @@ JamiAccount::handlePendingConversations()
                                                                              conversationId);
                 }
             } catch (const std::exception& e) {
+                emitSignal<DRing::ConversationSignal::OnConversationError>(getAccountID(),
+                                                                           conversationId,
+                                                                           EFETCH,
+                                                                           e.what());
                 JAMI_WARN("Something went wrong when cloning conversation: %s", e.what());
             }
             it = pendingConversationsFetch_.erase(it);
@@ -4134,7 +4136,9 @@ JamiAccount::addConversationMember(const std::string& conversationId,
     }
 
     if (it->second->isMember(contactUri, true)) {
-        JAMI_DBG("%s is already a member of %s, resend invite", contactUri.c_str(), conversationId.c_str());
+        JAMI_DBG("%s is already a member of %s, resend invite",
+                 contactUri.c_str(),
+                 conversationId.c_str());
         // Note: This should not be necessary, but if for whatever reason the other side didn't join
         // we should not forbid new invites
         sendTextMessage(contactUri, it->second->generateInvitation());
@@ -4146,24 +4150,29 @@ JamiAccount::addConversationMember(const std::string& conversationId,
         JAMI_WARN("Couldn't add %s to %s", contactUri.c_str(), conversationId.c_str());
         return false;
     }
-    it->second->loadMessages([w=weak(), conversationId, sendRequest, contactUri, commitId](auto&& messages) {
-        auto shared = w.lock();
-        if (not shared or messages.empty())
-            return; // should not happen
-        std::lock_guard<std::mutex> lk(shared->conversationsMtx_);
-        // Add a new member in the conversation
-        auto it = shared->conversations_.find(conversationId);
-        if (it == shared->conversations_.end()) {
-            return;
-        }
-        auto message = messages.front();
-        if (message.at("type") == "member")
-            shared->announceMemberMessage(conversationId, message);
-        emitSignal<DRing::ConversationSignal::MessageReceived>(shared->getAccountID(), conversationId, message);
-        if (sendRequest)
-            shared->sendTextMessage(contactUri, it->second->generateInvitation());
-        shared->sendMessageNotification(*it->second, commitId, true);
-    }, commitId, 1);
+    it->second->loadMessages(
+        [w = weak(), conversationId, sendRequest, contactUri, commitId](auto&& messages) {
+            auto shared = w.lock();
+            if (not shared or messages.empty())
+                return; // should not happen
+            std::lock_guard<std::mutex> lk(shared->conversationsMtx_);
+            // Add a new member in the conversation
+            auto it = shared->conversations_.find(conversationId);
+            if (it == shared->conversations_.end()) {
+                return;
+            }
+            auto message = messages.front();
+            if (message.at("type") == "member")
+                shared->announceMemberMessage(conversationId, message);
+            emitSignal<DRing::ConversationSignal::MessageReceived>(shared->getAccountID(),
+                                                                   conversationId,
+                                                                   message);
+            if (sendRequest)
+                shared->sendTextMessage(contactUri, it->second->generateInvitation());
+            shared->sendMessageNotification(*it->second, commitId, true);
+        },
+        commitId,
+        1);
     return true;
 }
 
@@ -4177,24 +4186,30 @@ JamiAccount::removeConversationMember(const std::string& conversationId,
     if (conversation != conversations_.end() && conversation->second) {
         auto lastCommit = conversation->second->lastCommitId();
         if (conversation->second->removeMember(contactUri, isDevice)) {
-            conversation->second->loadMessages([w=weak(), lastCommit, conversationId] (auto&& messages) {
-                if (auto shared = w.lock()) {
-                    std::reverse(messages.begin(), messages.end());
-                    // Announce new commits
-                    for (const auto& msg : messages) {
-                        if (msg.at("type") == "member")
-                            shared->announceMemberMessage(conversationId, msg);
-                        emitSignal<DRing::ConversationSignal::MessageReceived>(shared->getAccountID(),
-                                                                                conversationId,
-                                                                                msg);
+            conversation->second->loadMessages(
+                [w = weak(), conversationId](auto&& messages) {
+                    if (auto shared = w.lock()) {
+                        std::reverse(messages.begin(), messages.end());
+                        // Announce new commits
+                        auto lastId = std::string();
+                        for (const auto& msg : messages) {
+                            if (msg.at("type") == "member")
+                                shared->announceMemberMessage(conversationId, msg);
+                            emitSignal<DRing::ConversationSignal::MessageReceived>(
+                                shared->getAccountID(), conversationId, msg);
+                        }
+                        // Get last id
+                        if (messages.size() > 0)
+                            lastId = messages.rbegin()->at("id");
+                        std::lock_guard<std::mutex> lk(shared->conversationsMtx_);
+                        auto conversation = shared->conversations_.find(conversationId);
+                        // Send notification for others
+                        if (conversation != shared->conversations_.end() && conversation->second)
+                            shared->sendMessageNotification(*conversation->second, lastId, true);
                     }
-                    std::lock_guard<std::mutex> lk(shared->conversationsMtx_);
-                    auto conversation = shared->conversations_.find(conversationId);
-                    // Send notification for others
-                    if (conversation != shared->conversations_.end() && conversation->second)
-                        shared->sendMessageNotification(*conversation->second, lastCommit, true);
-                }
-            }, "", lastCommit);
+                },
+                "",
+                lastCommit);
             return true;
         }
     }
@@ -4238,13 +4253,16 @@ JamiAccount::sendMessage(const std::string& conversationId,
         if (!announce)
             return;
         if (!commitId.empty()) {
-            conversation->second->loadMessages([w=weak(), conversationId] (auto&& messages) {
-                auto shared = w.lock();
-                if (shared && !messages.empty())
-                    emitSignal<DRing::ConversationSignal::MessageReceived>(shared->getAccountID(),
-                                                                        conversationId,
-                                                                        messages.front());
-            }, commitId, 1);
+            conversation->second->loadMessages(
+                [w = weak(), conversationId](auto&& messages) {
+                    auto shared = w.lock();
+                    if (shared && !messages.empty())
+                        emitSignal<DRing::ConversationSignal::MessageReceived>(shared->getAccountID(),
+                                                                               conversationId,
+                                                                               messages.front());
+                },
+                commitId,
+                1);
             sendMessageNotification(*conversation->second, commitId, true);
         } else {
             JAMI_ERR("Failed to send message to conversation %s", conversationId.c_str());
@@ -4263,14 +4281,17 @@ JamiAccount::loadConversationMessages(const std::string& conversationId,
     std::lock_guard<std::mutex> lk(conversationsMtx_);
     auto conversation = conversations_.find(conversationId);
     if (conversation != conversations_.end() && conversation->second) {
-        conversation->second->loadMessages([w=weak(), conversationId, id](auto&& messages) {
-            if (auto shared = w.lock()) {
-                emitSignal<DRing::ConversationSignal::ConversationLoaded>(id,
-                                                                            shared->getAccountID(),
-                                                                            conversationId,
-                                                                            messages);
-            }
-        }, fromMessage, n);
+        conversation->second->loadMessages(
+            [w = weak(), conversationId, id](auto&& messages) {
+                if (auto shared = w.lock()) {
+                    emitSignal<DRing::ConversationSignal::ConversationLoaded>(id,
+                                                                              shared->getAccountID(),
+                                                                              conversationId,
+                                                                              messages);
+                }
+            },
+            fromMessage,
+            n);
     }
     return id;
 }
@@ -4352,25 +4373,26 @@ JamiAccount::fetchNewCommits(const std::string& peer,
         };
 
         if (gitSocket(deviceId, conversationId)) {
-            conversation->second
-                ->pull(deviceId,
-                       [deviceId,
-                        conversationId,
-                        w = weak(),
-                        announceMessages = std::move(announceMessages)](bool ok, auto messages) {
-                           auto shared = w.lock();
-                           if (!shared)
-                               return;
-                           if (!ok) {
-                               JAMI_WARN("[Account %s] Could not fetch new commit from %s for %s",
-                                         shared->getAccountID().c_str(),
-                                         deviceId.c_str(),
-                                         conversationId.c_str());
-                               shared->removeGitSocket(deviceId, conversationId);
-                           }
-                           if (!messages.empty())
-                               announceMessages(messages);
-                       }, commitId);
+            conversation->second->pull(
+                deviceId,
+                [deviceId,
+                 conversationId,
+                 w = weak(),
+                 announceMessages = std::move(announceMessages)](bool ok, auto messages) {
+                    auto shared = w.lock();
+                    if (!shared)
+                        return;
+                    if (!ok) {
+                        JAMI_WARN("[Account %s] Could not fetch new commit from %s for %s",
+                                  shared->getAccountID().c_str(),
+                                  deviceId.c_str(),
+                                  conversationId.c_str());
+                        shared->removeGitSocket(deviceId, conversationId);
+                    }
+                    if (!messages.empty())
+                        announceMessages(messages);
+                },
+                commitId);
         } else {
             lk.unlock();
             // Else we need to add a new gitSocket
@@ -4384,12 +4406,8 @@ JamiAccount::fetchNewCommits(const std::string& peer,
             connectionManager_->connectDevice(
                 DeviceId(deviceId),
                 "git://" + deviceId + "/" + conversationId,
-                [this,
-                 conversationId,
-                 commitId,
-                 announceMessages = std::move(
-                     announceMessages)](std::shared_ptr<ChannelSocket> socket,
-                                        const DeviceId& deviceId) {
+                [this, conversationId, commitId, announceMessages = std::move(announceMessages)](
+                    std::shared_ptr<ChannelSocket> socket, const DeviceId& deviceId) {
                     dht::ThreadPool::io().run([w = weak(),
                                                conversationId,
                                                socket = std::move(socket),
@@ -4406,28 +4424,28 @@ JamiAccount::fetchNewCommits(const std::string& peer,
                         if (socket) {
                             shared->addGitSocket(deviceId.toString(), conversationId, socket);
 
-                            conversation->second
-                                ->pull(deviceId.toString(),
-                                       [deviceId,
-                                        conversationId,
-                                        w,
-                                        announceMessages = std::move(
-                                            announceMessages)](bool ok, auto messages) {
-                                           auto shared = w.lock();
-                                           if (!shared)
-                                               return;
-                                           if (!ok) {
-                                               JAMI_WARN("[Account %s] Could not fetch new commit "
-                                                         "from %s for %s",
-                                                         shared->getAccountID().c_str(),
-                                                         deviceId.to_c_str(),
-                                                         conversationId.c_str());
-                                               shared->removeGitSocket(deviceId.toString(),
-                                                                       conversationId);
-                                           }
-                                           if (!messages.empty())
-                                               announceMessages(messages);
-                                       }, commitId);
+                            conversation->second->pull(
+                                deviceId.toString(),
+                                [deviceId,
+                                 conversationId,
+                                 w,
+                                 announceMessages = std::move(announceMessages)](bool ok,
+                                                                                 auto messages) {
+                                    auto shared = w.lock();
+                                    if (!shared)
+                                        return;
+                                    if (!ok) {
+                                        JAMI_WARN("[Account %s] Could not fetch new commit "
+                                                  "from %s for %s",
+                                                  shared->getAccountID().c_str(),
+                                                  deviceId.to_c_str(),
+                                                  conversationId.c_str());
+                                        shared->removeGitSocket(deviceId.toString(), conversationId);
+                                    }
+                                    if (!messages.empty())
+                                        announceMessages(messages);
+                                },
+                                commitId);
                         } else {
                             JAMI_ERR("[Account %s] Couldn't open a new git channel with %s for "
                                      "conversation %s",
@@ -4948,7 +4966,6 @@ JamiAccount::cacheSyncConnection(std::shared_ptr<ChannelSocket>&& socket,
                                         // Save the git socket
                                         addGitSocket(deviceId.toString(), convId, socket);
                                         checkConversationsEvents();
-                                        // TODO when do we remove the gitSocket?
                                     } else {
                                         lk.unlock();
                                         socket->shutdown();
@@ -4971,7 +4988,8 @@ JamiAccount::cacheSyncConnection(std::shared_ptr<ChannelSocket>&& socket,
                         std::lock_guard<std::mutex> lk(conversationsMtx_);
                         auto itConv = conversations_.find(convId);
                         if (itConv != conversations_.end() && !itConv->second->isRemoving()) {
-                            emitSignal<DRing::ConversationSignal::ConversationRemoved>(accountID_, convId);
+                            emitSignal<DRing::ConversationSignal::ConversationRemoved>(accountID_,
+                                                                                       convId);
                             itConv->second->setRemovingFlag();
                         }
                     }
@@ -5255,6 +5273,7 @@ JamiAccount::addCallHistoryMessage(const std::string& uri, uint64_t duration_ms)
 void
 JamiAccount::monitor() const
 {
+    std::lock_guard<std::mutex> lkCM(connManagerMtx_);
     if (connectionManager_)
         connectionManager_->monitor();
 }
diff --git a/test/unitTest/conversation/conversation.cpp b/test/unitTest/conversation/conversation.cpp
index f934574990..36d7b77f9e 100644
--- a/test/unitTest/conversation/conversation.cpp
+++ b/test/unitTest/conversation/conversation.cpp
@@ -26,6 +26,7 @@
 #include <streambuf>
 #include <git2.h>
 #include <filesystem>
+#include <msgpack.hpp>
 
 #include "manager.h"
 #include "jamidht/conversation.h"
@@ -40,6 +41,16 @@
 using namespace std::string_literals;
 using namespace DRing::Account;
 
+struct ConvInfoTest
+{
+    std::string id {};
+    time_t created {0};
+    time_t removed {0};
+    time_t erased {0};
+
+    MSGPACK_DEFINE_MAP(id, created, removed, erased)
+};
+
 namespace jami {
 namespace test {
 
@@ -50,9 +61,13 @@ public:
     static std::string name() { return "Conversation"; }
     void setUp();
     void tearDown();
-    void generateFakeVote(std::shared_ptr<JamiAccount> account,
-                          const std::string& convId,
-                          const std::string& votedUri);
+    void addVote(std::shared_ptr<JamiAccount> account,
+                 const std::string& convId,
+                 const std::string& votedUri,
+                 const std::string& content);
+    void simulateRemoval(std::shared_ptr<JamiAccount> account,
+                         const std::string& convId,
+                         const std::string& votedUri);
     void generateFakeInvite(std::shared_ptr<JamiAccount> account,
                             const std::string& convId,
                             const std::string& uri);
@@ -64,6 +79,10 @@ public:
     void commit(std::shared_ptr<JamiAccount> account,
                 const std::string& convId,
                 Json::Value& message);
+    std::string commitInRepo(const std::string& repoPath,
+                             std::shared_ptr<JamiAccount> account,
+                             const std::string& message);
+    std::string createFakeConversation(std::shared_ptr<JamiAccount> account);
 
     std::string aliceId;
     std::string bobId;
@@ -78,6 +97,7 @@ private:
     void testRemoveConversationNoMember();
     void testRemoveConversationWithMember();
     void testAddMember();
+    void testMemberAddedNoBadFile();
     void testAddOfflineMemberThenConnects();
     void testGetMembers();
     void testSendMessage();
@@ -88,33 +108,31 @@ private:
     void testSendMessageToMultipleParticipants();
     void testPingPongMessages();
     void testRemoveMember();
-    // void testBanDevice();
+    void testMemberBanNoBadFile();
     void testMemberTryToRemoveAdmin();
     void testBannedMemberCannotSendMessage();
     void testAddBannedMember();
     void testMemberCannotBanOther();
     void testCheckAdminFakeAVoteIsDetected();
+    void testVoteNonEmpty();
     void testAdminCannotKickTheirself();
-    // TODO void testBannedDeviceCannotSendMessageButMemberCan();
-    // TODO void testRevokedDeviceCannotSendMessage();
+    void testCommitUnauthorizedUser();
+    // LATER void testBanDevice();
+    // LATER void testBannedDeviceCannotSendMessageButMemberCan();
+    // LATER void testRevokedDeviceCannotSendMessage();
     // LATER void test2AdminsCannotBanEachOthers();
     // LATER void test2AdminsBanMembers();
     // LATER void test2AdminsBanOtherAdmin();
     // LATER void testAdminRemoveConversationShouldPromoteOther();
 
-    // TODO2 testCommitUnauthorizedUser
-    // TODO2 testNoBadFileInInitialCommit
+    void testNoBadFileInInitialCommit();
     void testPlainTextNoBadFile();
-    // TODO2 testVoteFromNonAdmin
     void testVoteNoBadFile();
     void testETooBigClone();
     void testETooBigFetch();
-    // TODO2 testVoteNonEmpty
-    // TODO2 testMemberAddedNoCertificate
-    // TODO2 void testMemberAddedNoBadFile();
-    // TODO2 void testMemberJoinsNoBadFile();
-    // TODO2 testMemberJoinsInviteRemoved
-    // TODO2 testMemberBanNoBadFile
+    void testMemberJoinsNoBadFile();
+    void testMemberAddedNoCertificate();
+    void testMemberJoinsInviteRemoved();
     void testAddContact();
     void testAddContactDeleteAndReAdd();
     void testFailAddMemberInOneToOne();
@@ -134,6 +152,7 @@ private:
     CPPUNIT_TEST(testRemoveConversationNoMember);
     CPPUNIT_TEST(testRemoveConversationWithMember);
     CPPUNIT_TEST(testAddMember);
+    CPPUNIT_TEST(testMemberAddedNoBadFile);
     CPPUNIT_TEST(testAddOfflineMemberThenConnects);
     CPPUNIT_TEST(testGetMembers);
     CPPUNIT_TEST(testSendMessage);
@@ -143,16 +162,23 @@ private:
     CPPUNIT_TEST(testSendMessageToMultipleParticipants);
     CPPUNIT_TEST(testPingPongMessages);
     CPPUNIT_TEST(testRemoveMember);
+    CPPUNIT_TEST(testMemberBanNoBadFile);
     CPPUNIT_TEST(testMemberTryToRemoveAdmin);
     CPPUNIT_TEST(testBannedMemberCannotSendMessage);
     CPPUNIT_TEST(testAddBannedMember);
     CPPUNIT_TEST(testMemberCannotBanOther);
     CPPUNIT_TEST(testCheckAdminFakeAVoteIsDetected);
+    CPPUNIT_TEST(testVoteNonEmpty);
     CPPUNIT_TEST(testAdminCannotKickTheirself);
+    CPPUNIT_TEST(testCommitUnauthorizedUser);
+    CPPUNIT_TEST(testNoBadFileInInitialCommit);
     CPPUNIT_TEST(testPlainTextNoBadFile);
     CPPUNIT_TEST(testVoteNoBadFile);
     CPPUNIT_TEST(testETooBigClone);
     CPPUNIT_TEST(testETooBigFetch);
+    CPPUNIT_TEST(testMemberJoinsNoBadFile);
+    CPPUNIT_TEST(testMemberAddedNoCertificate);
+    CPPUNIT_TEST(testMemberJoinsInviteRemoved);
     CPPUNIT_TEST(testAddContact);
     CPPUNIT_TEST(testAddContactDeleteAndReAdd);
     CPPUNIT_TEST(testFailAddMemberInOneToOne);
@@ -307,7 +333,6 @@ void
 ConversationTest::testGetConversation()
 {
     auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
-    auto aliceDeviceId = aliceAccount->currentDeviceId();
     auto uri = aliceAccount->getUsername();
     auto convId = aliceAccount->startConversation();
 
@@ -320,7 +345,6 @@ void
 ConversationTest::testGetConversationsAfterRm()
 {
     auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
-    auto aliceDeviceId = aliceAccount->currentDeviceId();
     auto uri = aliceAccount->getUsername();
 
     std::mutex mtx;
@@ -352,7 +376,6 @@ void
 ConversationTest::testRemoveInvalidConversation()
 {
     auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
-    auto aliceDeviceId = aliceAccount->currentDeviceId();
     auto uri = aliceAccount->getUsername();
 
     std::mutex mtx;
@@ -384,7 +407,6 @@ void
 ConversationTest::testRemoveConversationNoMember()
 {
     auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
-    auto aliceDeviceId = aliceAccount->currentDeviceId();
     auto uri = aliceAccount->getUsername();
 
     std::mutex mtx;
@@ -558,6 +580,56 @@ ConversationTest::testAddMember()
     CPPUNIT_ASSERT(fileutils::isFile(bobMember));
 }
 
+void
+ConversationTest::testMemberAddedNoBadFile()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto bobUri = bobAccount->getUsername();
+    auto convId = aliceAccount->startConversation();
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    bool conversationReady = false, requestReceived = false, errorDetected = false;
+    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 == bobId) {
+                conversationReady = true;
+                cv.notify_one();
+            }
+        }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>(
+        [&](const std::string& accountId,
+            const std::string& conversationId,
+            int code,
+            const std::string& /* what */) {
+            if (accountId == bobId && conversationId == convId && code == 1)
+                errorDetected = true;
+            cv.notify_one();
+        }));
+    DRing::registerSignalHandlers(confHandlers);
+    addFile(aliceAccount, convId, "BADFILE");
+    generateFakeInvite(aliceAccount, convId, bobUri);
+    // Generate conv request
+    aliceAccount->sendTextMessage(bobUri,
+                                  {{"application/invite+json",
+                                    "{\"conversationId\":\"" + convId + "\"}"}});
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; }));
+    errorDetected = false;
+    bobAccount->acceptConversationRequest(convId);
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return errorDetected; }));
+    DRing::unregisterSignalHandlers();
+}
+
 void
 ConversationTest::testAddOfflineMemberThenConnects()
 {
@@ -595,11 +667,11 @@ ConversationTest::testAddOfflineMemberThenConnects()
     CPPUNIT_ASSERT(requestReceived);
 
     carlaAccount->acceptConversationRequest(convId);
-    cv.wait_for(lk, std::chrono::seconds(30));
-    CPPUNIT_ASSERT(conversationReady);
+    cv.wait_for(lk, std::chrono::seconds(30), [&]() { return conversationReady; });
     auto clonedPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + carlaAccount->getAccountID()
                       + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
     CPPUNIT_ASSERT(fileutils::isDirectory(clonedPath));
+    DRing::unregisterSignalHandlers();
 }
 
 void
@@ -671,6 +743,7 @@ ConversationTest::testGetMembers()
     CPPUNIT_ASSERT(members[0]["role"] == "admin");
     CPPUNIT_ASSERT(members[1]["uri"] == bobUri);
     CPPUNIT_ASSERT(members[1]["role"] == "member");
+    DRing::unregisterSignalHandlers();
 }
 
 void
@@ -730,7 +803,7 @@ ConversationTest::testSendMessage()
     // Wait that alice sees Bob
     cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageAliceReceived == 1; });
 
-    aliceAccount->sendMessage(convId, std::string("hi"));
+    aliceAccount->sendMessage(convId, "hi"s);
     cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageBobReceived == 1; });
     DRing::unregisterSignalHandlers();
 }
@@ -760,10 +833,9 @@ ConversationTest::testSendMessageTriggerMessageReceived()
     DRing::registerSignalHandlers(confHandlers);
 
     auto convId = aliceAccount->startConversation();
-    cv.wait_for(lk, std::chrono::seconds(30));
-    CPPUNIT_ASSERT(conversationReady);
+    cv.wait_for(lk, std::chrono::seconds(30), [&] { return conversationReady; });
 
-    aliceAccount->sendMessage(convId, std::string("hi"));
+    aliceAccount->sendMessage(convId, "hi"s);
     cv.wait_for(lk, std::chrono::seconds(30), [&] { return messageReceived == 1; });
     CPPUNIT_ASSERT(messageReceived == 1);
     DRing::unregisterSignalHandlers();
@@ -793,7 +865,7 @@ ConversationTest::testMergeTwoDifferentHeads()
     confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>(
         [&](const std::string& accountId,
             const std::string& conversationId,
-            std::map<std::string, std::string> message) {
+            std::map<std::string, std::string> /* message */) {
             if (accountId == carlaId && conversationId == convId) {
                 carlaGotMessage = true;
             }
@@ -819,9 +891,9 @@ ConversationTest::testMergeTwoDifferentHeads()
     ConversationRepository repo(carlaAccount, convId);
     repo.join();
 
-    aliceAccount->sendMessage(convId, std::string("hi"));
-    aliceAccount->sendMessage(convId, std::string("sup"));
-    aliceAccount->sendMessage(convId, std::string("jami"));
+    aliceAccount->sendMessage(convId, "hi"s);
+    aliceAccount->sendMessage(convId, "sup"s);
+    aliceAccount->sendMessage(convId, "jami"s);
 
     // Start Carla, should merge and all messages should be there
     Manager::instance().sendRegister(carlaId, true);
@@ -859,6 +931,7 @@ ConversationTest::testGetRequests()
     auto requests = bobAccount->getConversationRequests();
     CPPUNIT_ASSERT(requests.size() == 1);
     CPPUNIT_ASSERT(requests.front()["id"] == convId);
+    DRing::unregisterSignalHandlers();
 }
 
 void
@@ -892,6 +965,7 @@ ConversationTest::testDeclineRequest()
     // Decline request
     auto requests = bobAccount->getConversationRequests();
     CPPUNIT_ASSERT(requests.size() == 0);
+    DRing::unregisterSignalHandlers();
 }
 
 void
@@ -982,7 +1056,7 @@ ConversationTest::testSendMessageToMultipleParticipants()
                + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
     CPPUNIT_ASSERT(fileutils::isDirectory(repoPath));
 
-    aliceAccount->sendMessage(convId, std::string("hi"));
+    aliceAccount->sendMessage(convId, "hi"s);
     CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&]() {
         return messageReceivedBob >= 1 && messageReceivedCarla >= 1;
     }));
@@ -1045,19 +1119,19 @@ ConversationTest::testPingPongMessages()
     CPPUNIT_ASSERT(fileutils::isDirectory(repoPath));
     messageBobReceived = 0;
     messageAliceReceived = 0;
-    aliceAccount->sendMessage(convId, std::string("ping"));
+    aliceAccount->sendMessage(convId, "ping"s);
     CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
         return messageBobReceived == 1 && messageAliceReceived == 1;
     }));
-    bobAccount->sendMessage(convId, std::string("pong"));
+    bobAccount->sendMessage(convId, "pong"s);
     CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
         return messageBobReceived == 2 && messageAliceReceived == 2;
     }));
-    bobAccount->sendMessage(convId, std::string("ping"));
+    bobAccount->sendMessage(convId, "ping"s);
     CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
         return messageBobReceived == 3 && messageAliceReceived == 3;
     }));
-    aliceAccount->sendMessage(convId, std::string("pong"));
+    aliceAccount->sendMessage(convId, "pong"s);
     CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
         return messageBobReceived == 4 && messageAliceReceived == 4;
     }));
@@ -1125,6 +1199,93 @@ ConversationTest::testRemoveMember()
     CPPUNIT_ASSERT(members.size() == 1);
     CPPUNIT_ASSERT(members[0]["uri"] == aliceAccount->getUsername());
     CPPUNIT_ASSERT(members[0]["role"] == "admin");
+    DRing::unregisterSignalHandlers();
+}
+
+void
+ConversationTest::testMemberBanNoBadFile()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto aliceUri = aliceAccount->getUsername();
+    auto bobUri = bobAccount->getUsername();
+    auto convId = aliceAccount->startConversation();
+    auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId);
+    auto carlaUri = carlaAccount->getUsername();
+    aliceAccount->trackBuddyPresence(carlaUri, true);
+    Manager::instance().sendRegister(carlaId, true);
+
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    bool conversationReady = false, requestReceived = false, memberMessageGenerated = false,
+         voteMessageGenerated = false, messageBobReceived = false, errorDetected = false;
+    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 == bobId && conversationId == convId) {
+                conversationReady = true;
+                cv.notify_one();
+            }
+        }));
+    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 == aliceId && conversationId == convId && message["type"] == "vote") {
+                voteMessageGenerated = true;
+            } else if (accountId == aliceId && conversationId == convId
+                       && message["type"] == "member") {
+                memberMessageGenerated = true;
+            } else if (accountId == bobId && conversationId == convId) {
+                messageBobReceived = true;
+            }
+            cv.notify_one();
+        }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>(
+        [&](const std::string& accountId,
+            const std::string& conversationId,
+            int code,
+            const std::string& /* what */) {
+            if (accountId == bobId && conversationId == convId && code == 3)
+                errorDetected = true;
+            cv.notify_one();
+        }));
+    DRing::registerSignalHandlers(confHandlers);
+    CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri));
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
+        return requestReceived && memberMessageGenerated;
+    }));
+    memberMessageGenerated = false;
+    bobAccount->acceptConversationRequest(convId);
+    CPPUNIT_ASSERT(
+        cv.wait_for(lk, std::chrono::seconds(30), [&]() { return memberMessageGenerated; }));
+    requestReceived = false;
+    CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, carlaUri));
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
+        return requestReceived && memberMessageGenerated;
+    }));
+    memberMessageGenerated = false;
+    messageBobReceived = false;
+    carlaAccount->acceptConversationRequest(convId);
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
+        return memberMessageGenerated && messageBobReceived;
+    }));
+
+    memberMessageGenerated = false;
+    voteMessageGenerated = false;
+    addFile(aliceAccount, convId, "BADFILE");
+    aliceAccount->removeConversationMember(convId, carlaUri);
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return errorDetected; }));
+    DRing::unregisterSignalHandlers();
 }
 
 /*void
@@ -1290,6 +1451,7 @@ ConversationTest::testMemberTryToRemoveAdmin()
     bobAccount->removeConversationMember(convId, aliceUri);
     auto members = aliceAccount->getConversationMembers(convId);
     CPPUNIT_ASSERT(members.size() == 2 && !memberMessageGenerated);
+    DRing::unregisterSignalHandlers();
 }
 
 void
@@ -1357,9 +1519,10 @@ ConversationTest::testBannedMemberCannotSendMessage()
 
     // Now check that alice doesn't receive a message from Bob
     aliceMessageReceived = false;
-    bobAccount->sendMessage(convId, std::string("hi"));
+    bobAccount->sendMessage(convId, "hi"s);
     CPPUNIT_ASSERT(
         !cv.wait_for(lk, std::chrono::seconds(30), [&]() { return aliceMessageReceived; }));
+    DRing::unregisterSignalHandlers();
 }
 
 void
@@ -1424,16 +1587,46 @@ ConversationTest::testAddBannedMember()
 
     // Then check that bobUri cannot be re-added
     CPPUNIT_ASSERT(!aliceAccount->addConversationMember(convId, bobUri));
+    DRing::unregisterSignalHandlers();
 }
 
 void
-ConversationTest::generateFakeVote(std::shared_ptr<JamiAccount> account,
-                                   const std::string& convId,
-                                   const std::string& votedUri)
+ConversationTest::addVote(std::shared_ptr<JamiAccount> account,
+                          const std::string& convId,
+                          const std::string& votedUri,
+                          const std::string& content)
+{
+    auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + account->getAccountID()
+                    + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
+    auto voteDirectory = repoPath + DIR_SEPARATOR_STR + "votes" + DIR_SEPARATOR_STR + "members";
+    auto voteFile = voteDirectory + DIR_SEPARATOR_STR + votedUri;
+    if (!fileutils::recursive_mkdir(voteDirectory, 0700)) {
+        return;
+    }
+
+    std::ofstream file(voteFile);
+    if (file.is_open()) {
+        file << content;
+        file.close();
+    }
+
+    Json::Value json;
+    json["uri"] = votedUri;
+    json["type"] = "vote";
+    Json::StreamWriterBuilder wbuilder;
+    wbuilder["commentStyle"] = "None";
+    wbuilder["indentation"] = "";
+    ConversationRepository cr(account->weak(), convId);
+    cr.commitMessage(Json::writeString(wbuilder, json));
+}
+
+void
+ConversationTest::simulateRemoval(std::shared_ptr<JamiAccount> account,
+                                  const std::string& convId,
+                                  const std::string& votedUri)
 {
     auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + account->getAccountID()
                     + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
-    // remove from member & add into banned without voting for the ban
     auto memberFile = repoPath + DIR_SEPARATOR_STR + "members" + DIR_SEPARATOR_STR + votedUri
                       + ".crt";
     auto bannedFile = repoPath + DIR_SEPARATOR_STR + "banned" + DIR_SEPARATOR_STR + "members"
@@ -1447,7 +1640,7 @@ ConversationTest::generateFakeVote(std::shared_ptr<JamiAccount> account,
 
     // git add -A
     git_index* index_ptr = nullptr;
-    git_strarray array = {0};
+    git_strarray array = {nullptr, 0};
     if (git_repository_index(&index_ptr, repo) < 0)
         return;
     GitIndex index {index_ptr, git_index_free};
@@ -1465,7 +1658,7 @@ ConversationTest::generateFakeVote(std::shared_ptr<JamiAccount> account,
     wbuilder["indentation"] = "";
     cr.commitMessage(Json::writeString(wbuilder, json));
 
-    account->sendMessage(convId, std::string("trigger the fake history to be pulled"));
+    account->sendMessage(convId, "trigger the fake history to be pulled"s);
 }
 
 void
@@ -1489,7 +1682,7 @@ ConversationTest::generateFakeInvite(std::shared_ptr<JamiAccount> account,
 
     // git add -A
     git_index* index_ptr = nullptr;
-    git_strarray array = {0};
+    git_strarray array = {nullptr, 0};
     if (git_repository_index(&index_ptr, repo) < 0)
         return;
     GitIndex index {index_ptr, git_index_free};
@@ -1507,8 +1700,9 @@ ConversationTest::generateFakeInvite(std::shared_ptr<JamiAccount> account,
     wbuilder["indentation"] = "";
     cr.commitMessage(Json::writeString(wbuilder, json));
 
-    account->sendMessage(convId, std::string("trigger the fake history to be pulled"));
+    account->sendMessage(convId, "trigger the fake history to be pulled"s);
 }
+
 void
 ConversationTest::addAll(std::shared_ptr<JamiAccount> account, const std::string& convId)
 {
@@ -1522,7 +1716,7 @@ ConversationTest::addAll(std::shared_ptr<JamiAccount> account, const std::string
 
     // git add -A
     git_index* index_ptr = nullptr;
-    git_strarray array = {0};
+    git_strarray array = {nullptr, 0};
     if (git_repository_index(&index_ptr, repo) < 0)
         return;
     GitIndex index {index_ptr, git_index_free};
@@ -1543,6 +1737,265 @@ ConversationTest::commit(std::shared_ptr<JamiAccount> account,
     cr.commitMessage(Json::writeString(wbuilder, message));
 }
 
+std::string
+ConversationTest::commitInRepo(const std::string& path,
+                               std::shared_ptr<JamiAccount> account,
+                               const std::string& msg)
+{
+    auto deviceId = std::string(account->currentDeviceId());
+    auto name = account->getDisplayName();
+    if (name.empty())
+        name = deviceId;
+
+    git_signature* sig_ptr = nullptr;
+    // Sign commit's buffer
+    if (git_signature_new(&sig_ptr, name.c_str(), deviceId.c_str(), std::time(nullptr), 0) < 0) {
+        JAMI_ERR("Unable to create a commit signature.");
+        return {};
+    }
+    GitSignature sig {sig_ptr, git_signature_free};
+
+    // Retrieve current index
+    git_index* index_ptr = nullptr;
+    git_repository* repo = nullptr;
+    // TODO share this repo with GitServer
+    if (git_repository_open(&repo, path.c_str()) != 0) {
+        JAMI_ERR("Could not open repository");
+        return {};
+    }
+
+    if (git_repository_index(&index_ptr, repo) < 0) {
+        JAMI_ERR("Could not open repository index");
+        return {};
+    }
+    GitIndex index {index_ptr, git_index_free};
+
+    git_oid tree_id;
+    if (git_index_write_tree(&tree_id, index.get()) < 0) {
+        JAMI_ERR("Unable to write initial tree from index");
+        return {};
+    }
+
+    git_tree* tree_ptr = nullptr;
+    if (git_tree_lookup(&tree_ptr, repo, &tree_id) < 0) {
+        JAMI_ERR("Could not look up initial tree");
+        return {};
+    }
+    GitTree tree = {tree_ptr, git_tree_free};
+
+    git_oid commit_id;
+    if (git_reference_name_to_id(&commit_id, repo, "HEAD") < 0) {
+        JAMI_ERR("Cannot get reference for HEAD");
+        return {};
+    }
+
+    git_commit* head_ptr = nullptr;
+    if (git_commit_lookup(&head_ptr, repo, &commit_id) < 0) {
+        JAMI_ERR("Could not look up HEAD commit");
+        return {};
+    }
+    GitCommit head_commit {head_ptr, git_commit_free};
+
+    git_buf to_sign = {};
+    const git_commit* head_ref[1] = {head_commit.get()};
+    if (git_commit_create_buffer(
+            &to_sign, repo, sig.get(), sig.get(), nullptr, msg.c_str(), tree.get(), 1, &head_ref[0])
+        < 0) {
+        JAMI_ERR("Could not create commit buffer");
+        return {};
+    }
+
+    // git commit -S
+    auto to_sign_vec = std::vector<uint8_t>(to_sign.ptr, to_sign.ptr + to_sign.size);
+    auto signed_buf = account->identity().first->sign(to_sign_vec);
+    std::string signed_str = base64::encode(signed_buf);
+    if (git_commit_create_with_signature(&commit_id,
+                                         repo,
+                                         to_sign.ptr,
+                                         signed_str.c_str(),
+                                         "signature")
+        < 0) {
+        JAMI_ERR("Could not sign commit");
+        return {};
+    }
+
+    // Move commit to main branch
+    git_reference* ref_ptr = nullptr;
+    if (git_reference_create(&ref_ptr, repo, "refs/heads/main", &commit_id, true, nullptr) < 0) {
+        JAMI_WARN("Could not move commit to main");
+    }
+    git_reference_free(ref_ptr);
+    git_repository_free(repo);
+
+    auto commit_str = git_oid_tostr_s(&commit_id);
+    if (commit_str) {
+        JAMI_INFO("New message added with id: %s", commit_str);
+        return commit_str;
+    }
+
+    return {};
+}
+
+std::string
+ConversationTest::createFakeConversation(std::shared_ptr<JamiAccount> account)
+{
+    auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + account->getAccountID()
+                    + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + "tmp";
+
+    git_repository* repo_ptr = nullptr;
+    git_repository_init_options opts;
+    git_repository_init_options_init(&opts, GIT_REPOSITORY_INIT_OPTIONS_VERSION);
+    opts.flags |= GIT_REPOSITORY_INIT_MKPATH;
+    opts.initial_head = "main";
+    if (git_repository_init_ext(&repo_ptr, repoPath.c_str(), &opts) < 0) {
+        JAMI_ERR("Couldn't create a git repository in %s", repoPath.c_str());
+    }
+    GitRepository repo {std::move(repo_ptr), git_repository_free};
+
+    // Add files
+    auto deviceId = std::string(account->currentDeviceId());
+
+    repoPath = git_repository_workdir(repo.get());
+    std::string adminsPath = repoPath + "admins";
+    std::string devicesPath = repoPath + "devices";
+    std::string crlsPath = repoPath + "CRLs" + DIR_SEPARATOR_STR + deviceId;
+
+    if (!fileutils::recursive_mkdir(adminsPath, 0700)) {
+        JAMI_ERR("Error when creating %s. Abort create conversations", adminsPath.c_str());
+    }
+
+    auto cert = account->identity().second;
+    auto deviceCert = cert->toString(false);
+    auto parentCert = cert->issuer;
+    if (!parentCert) {
+        JAMI_ERR("Parent cert is null!");
+    }
+
+    // /admins
+    std::string adminPath = adminsPath + DIR_SEPARATOR_STR + parentCert->getId().toString()
+                            + ".crt";
+    auto file = fileutils::ofstream(adminPath, std::ios::trunc | std::ios::binary);
+    if (!file.is_open()) {
+        JAMI_ERR("Could not write data to %s", adminPath.c_str());
+    }
+    file << parentCert->toString(true);
+    file.close();
+
+    if (!fileutils::recursive_mkdir(devicesPath, 0700)) {
+        JAMI_ERR("Error when creating %s. Abort create conversations", devicesPath.c_str());
+    }
+
+    // /devices
+    std::string devicePath = devicesPath + DIR_SEPARATOR_STR + cert->getId().toString() + ".crt";
+    file = fileutils::ofstream(devicePath, std::ios::trunc | std::ios::binary);
+    if (!file.is_open()) {
+        JAMI_ERR("Could not write data to %s", devicePath.c_str());
+    }
+    file << deviceCert;
+    file.close();
+
+    if (!fileutils::recursive_mkdir(crlsPath, 0700)) {
+        JAMI_ERR("Error when creating %s. Abort create conversations", crlsPath.c_str());
+    }
+
+    std::string badFile = repoPath + DIR_SEPARATOR_STR + "BAD";
+    file = fileutils::ofstream(badFile, std::ios::trunc | std::ios::binary);
+
+    addAll(account, "tmp");
+
+    JAMI_INFO("Initial files added in %s", repoPath.c_str());
+
+    std::string name = account->getDisplayName();
+    if (name.empty())
+        name = deviceId;
+
+    git_signature* sig_ptr = nullptr;
+    git_index* index_ptr = nullptr;
+    git_oid tree_id, commit_id;
+    git_tree* tree_ptr = nullptr;
+    git_buf to_sign = {};
+
+    // Sign commit's buffer
+    if (git_signature_new(&sig_ptr, name.c_str(), deviceId.c_str(), std::time(nullptr), 0) < 0) {
+        JAMI_ERR("Unable to create a commit signature.");
+    }
+    GitSignature sig {sig_ptr, git_signature_free};
+
+    if (git_repository_index(&index_ptr, repo.get()) < 0) {
+        JAMI_ERR("Could not open repository index");
+    }
+    GitIndex index {index_ptr, git_index_free};
+
+    if (git_index_write_tree(&tree_id, index.get()) < 0) {
+        JAMI_ERR("Unable to write initial tree from index");
+    }
+
+    if (git_tree_lookup(&tree_ptr, repo.get(), &tree_id) < 0) {
+        JAMI_ERR("Could not look up initial tree");
+    }
+    GitTree tree = {tree_ptr, git_tree_free};
+
+    Json::Value json;
+    json["mode"] = 1;
+    json["type"] = "initial";
+    Json::StreamWriterBuilder wbuilder;
+    wbuilder["commentStyle"] = "None";
+    wbuilder["indentation"] = "";
+
+    if (git_commit_create_buffer(&to_sign,
+                                 repo.get(),
+                                 sig.get(),
+                                 sig.get(),
+                                 nullptr,
+                                 Json::writeString(wbuilder, json).c_str(),
+                                 tree.get(),
+                                 0,
+                                 nullptr)
+        < 0) {
+        JAMI_ERR("Could not create initial buffer");
+        return {};
+    }
+
+    auto to_sign_vec = std::vector<uint8_t>(to_sign.ptr, to_sign.ptr + to_sign.size);
+    auto signed_buf = account->identity().first->sign(to_sign_vec);
+    std::string signed_str = base64::encode(signed_buf);
+
+    // git commit -S
+    if (git_commit_create_with_signature(&commit_id,
+                                         repo.get(),
+                                         to_sign.ptr,
+                                         signed_str.c_str(),
+                                         "signature")
+        < 0) {
+        JAMI_ERR("Could not sign initial commit");
+        return {};
+    }
+
+    // Move commit to main branch
+    git_commit* commit = nullptr;
+    if (git_commit_lookup(&commit, repo.get(), &commit_id) == 0) {
+        git_reference* ref = nullptr;
+        git_branch_create(&ref, repo.get(), "main", commit, true);
+        git_commit_free(commit);
+        git_reference_free(ref);
+    }
+
+    auto commit_str = git_oid_tostr_s(&commit_id);
+
+    auto finalRepo = fileutils::get_data_dir() + DIR_SEPARATOR_STR + account->getAccountID()
+                     + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + commit_str;
+    std::rename(repoPath.c_str(), finalRepo.c_str());
+
+    file = std::ofstream(fileutils::get_data_dir() + DIR_SEPARATOR_STR + account->getAccountID()
+                             + DIR_SEPARATOR_STR + "convInfo",
+                         std::ios::trunc | std::ios::binary);
+
+    std::vector<ConvInfoTest> test;
+    test.emplace_back(ConvInfoTest {commit_str, std::time(nullptr), 0, 0});
+    msgpack::pack(file, test);
+    return commit_str;
+}
+
 void
 ConversationTest::addFile(std::shared_ptr<JamiAccount> account,
                           const std::string& convId,
@@ -1567,7 +2020,7 @@ ConversationTest::addFile(std::shared_ptr<JamiAccount> account,
 
     // git add -A
     git_index* index_ptr = nullptr;
-    git_strarray array = {0};
+    git_strarray array = {nullptr, 0};
     if (git_repository_index(&index_ptr, repo) < 0)
         return;
     GitIndex index {index_ptr, git_index_free};
@@ -1593,7 +2046,7 @@ ConversationTest::testMemberCannotBanOther()
     std::condition_variable cv;
     std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
     bool conversationReady = false, requestReceived = false, memberMessageGenerated = false,
-         voteMessageGenerated = false, messageBobReceived = false;
+         voteMessageGenerated = false, messageBobReceived = false, errorDetected = false;
     confHandlers.insert(
         DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>(
             [&](const std::string& /*accountId*/,
@@ -1623,6 +2076,15 @@ ConversationTest::testMemberCannotBanOther()
             }
             cv.notify_one();
         }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>(
+        [&](const std::string& /* accountId */,
+            const std::string& /* conversationId */,
+            int code,
+            const std::string& /* what */) {
+            if (code == 3)
+                errorDetected = true;
+            cv.notify_one();
+        }));
     DRing::registerSignalHandlers(confHandlers);
     CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri));
     CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
@@ -1645,11 +2107,13 @@ ConversationTest::testMemberCannotBanOther()
     }));
 
     // Now Carla remove Bob as a member
+    errorDetected = false;
     messageBobReceived = false;
-    generateFakeVote(carlaAccount, convId, bobUri);
-    CPPUNIT_ASSERT(!cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageBobReceived; }));
+    // remove from member & add into banned without voting for the ban
+    simulateRemoval(carlaAccount, convId, bobUri);
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return errorDetected; }));
 
-    aliceAccount->sendMessage(convId, std::string("hi"));
+    aliceAccount->sendMessage(convId, "hi"s);
     CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageBobReceived; }));
 }
 
@@ -1671,7 +2135,7 @@ ConversationTest::testCheckAdminFakeAVoteIsDetected()
     std::condition_variable cv;
     std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
     bool conversationReady = false, requestReceived = false, memberMessageGenerated = false,
-         voteMessageGenerated = false, messageBobReceived = false;
+         voteMessageGenerated = false, messageBobReceived = false, errorDetected = false;
     confHandlers.insert(
         DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>(
             [&](const std::string& /*accountId*/,
@@ -1701,6 +2165,15 @@ ConversationTest::testCheckAdminFakeAVoteIsDetected()
             }
             cv.notify_one();
         }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>(
+        [&](const std::string& /* accountId */,
+            const std::string& /* conversationId */,
+            int code,
+            const std::string& /* what */) {
+            if (code == 3)
+                errorDetected = true;
+            cv.notify_one();
+        }));
     DRing::registerSignalHandlers(confHandlers);
     CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri));
     CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
@@ -1723,19 +2196,189 @@ ConversationTest::testCheckAdminFakeAVoteIsDetected()
     }));
 
     // Now Alice remove Carla without a vote. Bob will not receive the message
-    messageBobReceived = false;
-    generateFakeVote(aliceAccount, convId, carlaUri);
-    CPPUNIT_ASSERT(!cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageBobReceived; }));
+    errorDetected = false;
+    simulateRemoval(aliceAccount, convId, carlaUri);
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return errorDetected; }));
 }
 
 void
-ConversationTest::testAdminCannotKickTheirself()
+ConversationTest::testVoteNonEmpty()
 {
     auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
     auto aliceUri = aliceAccount->getUsername();
+    auto bobUri = bobAccount->getUsername();
     auto convId = aliceAccount->startConversation();
-    std::mutex mtx;
-    std::unique_lock<std::mutex> lk {mtx};
+    auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId);
+    auto carlaUri = carlaAccount->getUsername();
+    aliceAccount->trackBuddyPresence(carlaUri, true);
+    Manager::instance().sendRegister(carlaId, true);
+
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    bool conversationReady = false, requestReceived = false, memberMessageGenerated = false,
+         voteMessageGenerated = false, messageBobReceived = false, errorDetected = false;
+    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 == bobId && conversationId == convId) {
+                conversationReady = true;
+                cv.notify_one();
+            }
+        }));
+    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 == aliceId && conversationId == convId && message["type"] == "vote") {
+                voteMessageGenerated = true;
+            } else if (accountId == aliceId && conversationId == convId
+                       && message["type"] == "member") {
+                memberMessageGenerated = true;
+            } else if (accountId == bobId && conversationId == convId) {
+                messageBobReceived = true;
+            }
+            cv.notify_one();
+        }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>(
+        [&](const std::string& /* accountId */,
+            const std::string& /* conversationId */,
+            int code,
+            const std::string& /* what */) {
+            if (code == 3)
+                errorDetected = true;
+            cv.notify_one();
+        }));
+    DRing::registerSignalHandlers(confHandlers);
+    CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri));
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
+        return requestReceived && memberMessageGenerated;
+    }));
+    memberMessageGenerated = false;
+    bobAccount->acceptConversationRequest(convId);
+    CPPUNIT_ASSERT(
+        cv.wait_for(lk, std::chrono::seconds(30), [&]() { return memberMessageGenerated; }));
+    requestReceived = false;
+    CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, carlaUri));
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
+        return requestReceived && memberMessageGenerated;
+    }));
+    memberMessageGenerated = false;
+    messageBobReceived = false;
+    carlaAccount->acceptConversationRequest(convId);
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
+        return memberMessageGenerated && messageBobReceived;
+    }));
+
+    // Now Alice removes Carla with a non empty file
+    errorDetected = false;
+    addVote(aliceAccount, convId, carlaUri, "CONTENT");
+    simulateRemoval(aliceAccount, convId, carlaUri);
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return errorDetected; }));
+}
+
+void
+ConversationTest::testCommitUnauthorizedUser()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId);
+    auto bobUri = bobAccount->getUsername();
+
+    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;
+    bool errorDetected = 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 == bobId) {
+                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 == bobId) {
+                conversationReady = true;
+                cv.notify_one();
+            }
+        }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>(
+        [&](const std::string& /* accountId */,
+            const std::string& /* conversationId */,
+            int code,
+            const std::string& /* what */) {
+            if (code == 3)
+                errorDetected = true;
+            cv.notify_one();
+        }));
+    DRing::registerSignalHandlers(confHandlers);
+
+    auto convId = aliceAccount->startConversation();
+
+    CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri));
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; }));
+
+    bobAccount->acceptConversationRequest(convId);
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return conversationReady; }));
+
+    // Assert that repository exists
+    auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + bobAccount->getAccountID()
+                    + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
+    CPPUNIT_ASSERT(fileutils::isDirectory(repoPath));
+    // Wait that alice sees Bob
+    CPPUNIT_ASSERT(
+        cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageAliceReceived == 1; }));
+
+    // Add commit from invalid user
+    Json::Value root;
+    root["type"] = "text/plain";
+    root["body"] = "hi";
+    Json::StreamWriterBuilder wbuilder;
+    wbuilder["commentStyle"] = "None";
+    wbuilder["indentation"] = "";
+    auto message = Json::writeString(wbuilder, root);
+    commitInRepo(repoPath, carlaAccount, message);
+
+    errorDetected = false;
+    bobAccount->sendMessage(convId, "hi"s);
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return errorDetected; }));
+    DRing::unregisterSignalHandlers();
+}
+
+void
+ConversationTest::testAdminCannotKickTheirself()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto aliceUri = aliceAccount->getUsername();
+    auto convId = aliceAccount->startConversation();
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
     std::condition_variable cv;
     std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
     bool conversationReady = false, requestReceived = false, memberMessageGenerated = false,
@@ -1779,11 +2422,14 @@ ConversationTest::testAdminCannotKickTheirself()
 }
 
 void
-ConversationTest::testPlainTextNoBadFile()
+ConversationTest::testNoBadFileInInitialCommit()
 {
     auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
-    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
-    auto bobUri = bobAccount->getUsername();
+    auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId);
+    auto carlaUri = carlaAccount->getUsername();
+    auto aliceUri = aliceAccount->getUsername();
+
+    auto convId = createFakeConversation(carlaAccount);
 
     std::mutex mtx;
     std::unique_lock<std::mutex> lk {mtx};
@@ -1791,7 +2437,8 @@ ConversationTest::testPlainTextNoBadFile()
     std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
     auto messageBobReceived = 0, messageAliceReceived = 0;
     bool requestReceived = false;
-    bool conversationReady = false;
+    bool carlaConnected = false;
+    bool errorDetected = false;
     confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>(
         [&](const std::string& accountId,
             const std::string& /* conversationId */,
@@ -1811,6 +2458,74 @@ ConversationTest::testPlainTextNoBadFile()
                 requestReceived = true;
                 cv.notify_one();
             }));
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>(
+            [&](const std::string&, const std::map<std::string, std::string>&) {
+                auto details = carlaAccount->getVolatileAccountDetails();
+                auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS];
+                if (daemonStatus == "REGISTERED") {
+                    carlaConnected = true;
+                    cv.notify_one();
+                }
+            }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>(
+        [&](const std::string& /* accountId */,
+            const std::string& /* conversationId */,
+            int code,
+            const std::string& /* what */) {
+            if (code == 3)
+                errorDetected = true;
+            cv.notify_one();
+        }));
+    DRing::registerSignalHandlers(confHandlers);
+
+    Manager::instance().sendRegister(carlaId, true);
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return carlaConnected; }));
+    JAMI_ERR("@@@@@@@@@@@22");
+    CPPUNIT_ASSERT(carlaAccount->addConversationMember(convId, aliceUri));
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; }));
+
+    aliceAccount->acceptConversationRequest(convId);
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return errorDetected; }));
+}
+
+void
+ConversationTest::testPlainTextNoBadFile()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto bobUri = bobAccount->getUsername();
+
+    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;
+    bool memberMessageGenerated = false;
+    bool requestReceived = false;
+    bool conversationReady = false;
+    bool errorDetected = false;
+    std::string convId = aliceAccount->startConversation();
+    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 == bobId) {
+                messageBobReceived += 1;
+            } else if (accountId == aliceId && conversationId == convId
+                       && message["type"] == "member") {
+                memberMessageGenerated = true;
+            }
+            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 == bobId) {
@@ -1818,24 +2533,37 @@ ConversationTest::testPlainTextNoBadFile()
                 cv.notify_one();
             }
         }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>(
+        [&](const std::string& /* accountId */,
+            const std::string& /* conversationId */,
+            int code,
+            const std::string& /* what */) {
+            if (code == 3)
+                errorDetected = true;
+            cv.notify_one();
+        }));
     DRing::registerSignalHandlers(confHandlers);
 
-    auto convId = aliceAccount->startConversation();
-
     CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri));
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
+        return requestReceived && memberMessageGenerated;
+    }));
+    memberMessageGenerated = false;
 
     bobAccount->acceptConversationRequest(convId);
-    cv.wait_for(lk, std::chrono::seconds(30));
-    CPPUNIT_ASSERT(conversationReady);
-    // Wait that alice sees Bob
-    cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageAliceReceived == 1; });
+    cv.wait_for(lk, std::chrono::seconds(30), [&] {
+        return conversationReady && memberMessageGenerated;
+    });
 
     addFile(aliceAccount, convId, "BADFILE");
-    aliceAccount->sendMessage(convId, std::string("hi"));
+    Json::Value root;
+    root["type"] = "text/plain";
+    root["body"] = "hi";
+    commit(aliceAccount, convId, root);
+    errorDetected = false;
+    aliceAccount->sendMessage(convId, "hi"s);
     // Check not received due to the unwanted file
-    CPPUNIT_ASSERT(
-        !cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageBobReceived == 1; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return errorDetected; }));
     DRing::unregisterSignalHandlers();
 }
 
@@ -1924,7 +2652,7 @@ ConversationTest::testVoteNoBadFile()
     }));
 
     messageCarlaReceived = false;
-    bobAccount->sendMessage(convId, std::string("final"));
+    bobAccount->sendMessage(convId, "final"s);
     CPPUNIT_ASSERT(
         cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageCarlaReceived; }));
 }
@@ -1943,6 +2671,7 @@ ConversationTest::testETooBigClone()
     auto messageBobReceived = 0, messageAliceReceived = 0;
     bool requestReceived = false;
     bool conversationReady = false;
+    bool errorDetected = false;
     confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>(
         [&](const std::string& accountId,
             const std::string& /* conversationId */,
@@ -1969,6 +2698,15 @@ ConversationTest::testETooBigClone()
                 cv.notify_one();
             }
         }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>(
+        [&](const std::string& /* accountId */,
+            const std::string& /* conversationId */,
+            int code,
+            const std::string& /* what */) {
+            if (code == 3)
+                errorDetected = true;
+            cv.notify_one();
+        }));
     DRing::registerSignalHandlers(confHandlers);
 
     auto convId = aliceAccount->startConversation();
@@ -1987,8 +2725,9 @@ ConversationTest::testETooBigClone()
     CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri));
     CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; }));
 
+    errorDetected = false;
     bobAccount->acceptConversationRequest(convId);
-    CPPUNIT_ASSERT(!cv.wait_for(lk, std::chrono::seconds(30), [&]() { return conversationReady; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return errorDetected; }));
     DRing::unregisterSignalHandlers();
 }
 
@@ -2006,6 +2745,7 @@ ConversationTest::testETooBigFetch()
     auto messageBobReceived = 0, messageAliceReceived = 0;
     bool requestReceived = false;
     bool conversationReady = false;
+    bool errorDetected = false;
     confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>(
         [&](const std::string& accountId,
             const std::string& /* conversationId */,
@@ -2032,6 +2772,15 @@ ConversationTest::testETooBigFetch()
                 cv.notify_one();
             }
         }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>(
+        [&](const std::string& /* accountId */,
+            const std::string& /* conversationId */,
+            int code,
+            const std::string& /* what */) {
+            if (code == 3)
+                errorDetected = true;
+            cv.notify_one();
+        }));
     DRing::registerSignalHandlers(confHandlers);
 
     auto convId = aliceAccount->startConversation();
@@ -2049,6 +2798,7 @@ ConversationTest::testETooBigFetch()
                     + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
     std::ofstream bad(repoPath + DIR_SEPARATOR_STR + "BADFILE");
     CPPUNIT_ASSERT(bad.is_open());
+    errorDetected = false;
     for (int i = 0; i < 300 * 1024 * 1024; ++i)
         bad << "A";
     bad.close();
@@ -2059,9 +2809,247 @@ ConversationTest::testETooBigFetch()
     json["type"] = "text/plain";
     commit(aliceAccount, convId, json);
 
-    aliceAccount->sendMessage(convId, std::string("hi"));
-    CPPUNIT_ASSERT(
-        !cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageBobReceived == 1; }));
+    aliceAccount->sendMessage(convId, "hi"s);
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return errorDetected; }));
+    DRing::unregisterSignalHandlers();
+}
+
+void
+ConversationTest::testMemberJoinsNoBadFile()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId);
+    auto carlaUri = carlaAccount->getUsername();
+    aliceAccount->trackBuddyPresence(carlaUri, true);
+    auto convId = aliceAccount->startConversation();
+
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    bool conversationReady = false, errorDetected = false, carlaConnected = false;
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& accountId, const std::string& /* conversationId */) {
+            if (accountId == carlaId) {
+                conversationReady = true;
+                cv.notify_one();
+            }
+        }));
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>(
+            [&](const std::string&, const std::map<std::string, std::string>&) {
+                auto details = carlaAccount->getVolatileAccountDetails();
+                auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS];
+                if (daemonStatus == "REGISTERED") {
+                    carlaConnected = true;
+                    cv.notify_one();
+                }
+            }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>(
+        [&](const std::string& /* accountId */,
+            const std::string& /* conversationId */,
+            int code,
+            const std::string& /* what */) {
+            if (code == 3)
+                errorDetected = true;
+            cv.notify_one();
+        }));
+    DRing::registerSignalHandlers(confHandlers);
+
+    CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, carlaUri, false));
+
+    // Cp conversations & convInfo
+    auto repoPathAlice = fileutils::get_data_dir() + DIR_SEPARATOR_STR
+                         + aliceAccount->getAccountID() + DIR_SEPARATOR_STR + "conversations";
+    auto repoPathCarla = fileutils::get_data_dir() + DIR_SEPARATOR_STR
+                         + carlaAccount->getAccountID() + DIR_SEPARATOR_STR + "conversations";
+    std::filesystem::copy(repoPathAlice, repoPathCarla, std::filesystem::copy_options::recursive);
+    auto ciPathAlice = fileutils::get_data_dir() + DIR_SEPARATOR_STR + aliceAccount->getAccountID()
+                       + DIR_SEPARATOR_STR + "convInfo";
+    auto ciPathCarla = fileutils::get_data_dir() + DIR_SEPARATOR_STR + carlaAccount->getAccountID()
+                       + DIR_SEPARATOR_STR + "convInfo";
+    std::filesystem::copy(ciPathAlice, ciPathCarla);
+
+    // Accept for alice and makes different heads
+    addFile(carlaAccount, convId, "BADFILE");
+    ConversationRepository repo(carlaAccount, convId);
+    repo.join();
+
+    // Start Carla, should merge and all messages should be there
+    Manager::instance().sendRegister(carlaId, true);
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return carlaConnected; }));
+
+    carlaAccount->sendMessage(convId, "hi"s);
+    errorDetected = false;
+
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&] { return errorDetected; }));
+    DRing::unregisterSignalHandlers();
+}
+
+void
+ConversationTest::testMemberAddedNoCertificate()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId);
+    auto carlaUri = carlaAccount->getUsername();
+    aliceAccount->trackBuddyPresence(carlaUri, true);
+    auto convId = aliceAccount->startConversation();
+
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    bool conversationReady = false, errorDetected = false, carlaConnected = false;
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& accountId, const std::string& /* conversationId */) {
+            if (accountId == carlaId) {
+                conversationReady = true;
+                cv.notify_one();
+            }
+        }));
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>(
+            [&](const std::string&, const std::map<std::string, std::string>&) {
+                auto details = carlaAccount->getVolatileAccountDetails();
+                auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS];
+                if (daemonStatus == "REGISTERED") {
+                    carlaConnected = true;
+                    cv.notify_one();
+                }
+            }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>(
+        [&](const std::string& /* accountId */,
+            const std::string& /* conversationId */,
+            int code,
+            const std::string& /* what */) {
+            if (code == 3)
+                errorDetected = true;
+            cv.notify_one();
+        }));
+    DRing::registerSignalHandlers(confHandlers);
+
+    CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, carlaUri, false));
+
+    // Cp conversations & convInfo
+    auto repoPathAlice = fileutils::get_data_dir() + DIR_SEPARATOR_STR
+                         + aliceAccount->getAccountID() + DIR_SEPARATOR_STR + "conversations";
+    auto repoPathCarla = fileutils::get_data_dir() + DIR_SEPARATOR_STR
+                         + carlaAccount->getAccountID() + DIR_SEPARATOR_STR + "conversations";
+    std::filesystem::copy(repoPathAlice, repoPathCarla, std::filesystem::copy_options::recursive);
+    auto ciPathAlice = fileutils::get_data_dir() + DIR_SEPARATOR_STR + aliceAccount->getAccountID()
+                       + DIR_SEPARATOR_STR + "convInfo";
+    auto ciPathCarla = fileutils::get_data_dir() + DIR_SEPARATOR_STR + carlaAccount->getAccountID()
+                       + DIR_SEPARATOR_STR + "convInfo";
+    std::filesystem::copy(ciPathAlice, ciPathCarla);
+
+    // Remove invite but do not add member certificate
+    std::string invitedPath = repoPathCarla + "invited";
+    fileutils::remove(fileutils::getFullPath(invitedPath, carlaUri));
+
+    Json::Value json;
+    json["action"] = "join";
+    json["uri"] = carlaUri;
+    json["type"] = "member";
+    Json::StreamWriterBuilder wbuilder;
+    wbuilder["commentStyle"] = "None";
+    wbuilder["indentation"] = "";
+    ConversationRepository cr(carlaAccount->weak(), convId);
+    cr.commitMessage(Json::writeString(wbuilder, json));
+
+    // Start Carla, should merge and all messages should be there
+    Manager::instance().sendRegister(carlaId, true);
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return carlaConnected; }));
+
+    carlaAccount->sendMessage(convId, "hi"s);
+    errorDetected = false;
+
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&] { return errorDetected; }));
+    DRing::unregisterSignalHandlers();
+}
+
+void
+ConversationTest::testMemberJoinsInviteRemoved()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId);
+    auto carlaUri = carlaAccount->getUsername();
+    aliceAccount->trackBuddyPresence(carlaUri, true);
+    auto convId = aliceAccount->startConversation();
+
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    bool conversationReady = false, errorDetected = false, carlaConnected = false;
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& accountId, const std::string& /* conversationId */) {
+            if (accountId == carlaId) {
+                conversationReady = true;
+                cv.notify_one();
+            }
+        }));
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>(
+            [&](const std::string&, const std::map<std::string, std::string>&) {
+                auto details = carlaAccount->getVolatileAccountDetails();
+                auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS];
+                if (daemonStatus == "REGISTERED") {
+                    carlaConnected = true;
+                    cv.notify_one();
+                }
+            }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>(
+        [&](const std::string& /* accountId */,
+            const std::string& /* conversationId */,
+            int code,
+            const std::string& /* what */) {
+            if (code == 3)
+                errorDetected = true;
+            cv.notify_one();
+        }));
+    DRing::registerSignalHandlers(confHandlers);
+
+    CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, carlaUri, false));
+
+    // Cp conversations & convInfo
+    auto repoPathAlice = fileutils::get_data_dir() + DIR_SEPARATOR_STR
+                         + aliceAccount->getAccountID() + DIR_SEPARATOR_STR + "conversations";
+    auto repoPathCarla = fileutils::get_data_dir() + DIR_SEPARATOR_STR
+                         + carlaAccount->getAccountID() + DIR_SEPARATOR_STR + "conversations";
+    std::filesystem::copy(repoPathAlice, repoPathCarla, std::filesystem::copy_options::recursive);
+    auto ciPathAlice = fileutils::get_data_dir() + DIR_SEPARATOR_STR + aliceAccount->getAccountID()
+                       + DIR_SEPARATOR_STR + "convInfo";
+    auto ciPathCarla = fileutils::get_data_dir() + DIR_SEPARATOR_STR + carlaAccount->getAccountID()
+                       + DIR_SEPARATOR_STR + "convInfo";
+    std::filesystem::copy(ciPathAlice, ciPathCarla);
+
+    // Let invited, but add /members + /devices
+    auto cert = carlaAccount->identity().second;
+    auto parentCert = cert->issuer;
+    auto uri = parentCert->getId().toString();
+    std::string membersPath = repoPathCarla + "members" + DIR_SEPARATOR_STR;
+    std::string memberFile = membersPath + DIR_SEPARATOR_STR + carlaUri + ".crt";
+    // Add members/uri.crt
+    fileutils::recursive_mkdir(membersPath, 0700);
+    auto file = fileutils::ofstream(memberFile, std::ios::trunc | std::ios::binary);
+    file << parentCert->toString(true);
+    file.close();
+
+    addAll(carlaAccount, convId);
+    Json::Value json;
+    json["action"] = "join";
+    json["uri"] = carlaUri;
+    json["type"] = "member";
+    commit(carlaAccount, convId, json);
+
+    // Start Carla, should merge and all messages should be there
+    Manager::instance().sendRegister(carlaId, true);
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return carlaConnected; }));
+
+    carlaAccount->sendMessage(convId, "hi"s);
+    errorDetected = false;
+
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&] { return errorDetected; }));
     DRing::unregisterSignalHandlers();
 }
 
@@ -2264,7 +3252,8 @@ ConversationTest::testUnknownModeDetected()
     std::unique_lock<std::mutex> lk {mtx};
     std::condition_variable cv;
     std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
-    bool conversationReady = false, requestReceived = false, memberMessageGenerated = false;
+    bool conversationReady = false, requestReceived = false, memberMessageGenerated = false,
+         errorDetected = false;
     confHandlers.insert(
         DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>(
             [&](const std::string& /*accountId*/,
@@ -2289,11 +3278,21 @@ ConversationTest::testUnknownModeDetected()
                 cv.notify_one();
             }
         }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>(
+        [&](const std::string& /* accountId */,
+            const std::string& /* conversationId */,
+            int code,
+            const std::string& /* what */) {
+            if (code == 1)
+                errorDetected = true;
+            cv.notify_one();
+        }));
     DRing::registerSignalHandlers(confHandlers);
     CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri));
     CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; }));
+    errorDetected = false;
     bobAccount->acceptConversationRequest(convId);
-    CPPUNIT_ASSERT(!cv.wait_for(lk, std::chrono::seconds(30), [&]() { return conversationReady; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return errorDetected; }));
 }
 
 void
@@ -2418,7 +3417,6 @@ ConversationTest::testBanContact()
 
     memberMessageGenerated = false;
     bobAccount->removeContact(aliceUri, true);
-    cv.wait_for(lk, std::chrono::seconds(10));
     CPPUNIT_ASSERT(
         !cv.wait_for(lk, std::chrono::seconds(30), [&]() { return memberMessageGenerated; }));
     auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + bobAccount->getAccountID()
@@ -2440,7 +3438,7 @@ ConversationTest::testOneToOneFetchWithNewMemberRefused()
     std::condition_variable cv;
     std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
     bool conversationReady = false, requestReceived = false, memberMessageGenerated = false,
-         messageBob = false;
+         messageBob = false, errorDetected = false;
     std::string convId = "";
     confHandlers.insert(DRing::exportable_callback<DRing::ConfigurationSignal::IncomingTrustRequest>(
         [&](const std::string& account_id,
@@ -2472,6 +3470,15 @@ ConversationTest::testOneToOneFetchWithNewMemberRefused()
             }
             cv.notify_one();
         }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>(
+        [&](const std::string& /* accountId */,
+            const std::string& /* conversationId */,
+            int code,
+            const std::string& /* what */) {
+            if (code == 3)
+                errorDetected = true;
+            cv.notify_one();
+        }));
     DRing::registerSignalHandlers(confHandlers);
     aliceAccount->addContact(bobUri);
     aliceAccount->sendTrustRequest(bobUri, {});
@@ -2484,9 +3491,9 @@ ConversationTest::testOneToOneFetchWithNewMemberRefused()
         return conversationReady && memberMessageGenerated;
     }));
 
-    messageBob = false;
+    errorDetected = false;
     generateFakeInvite(aliceAccount, convId, carlaUri);
-    CPPUNIT_ASSERT(!cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageBob; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return errorDetected; }));
 }
 
 void
-- 
GitLab