Line data Source code
1 : /*
2 : * Copyright (C) 2024 Savoir-faire Linux Inc.
3 : * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
4 : *
5 : * This program is free software; you can redistribute it and/or modify
6 : * it under the terms of the GNU General Public License as published by
7 : * the Free Software Foundation; either version 3 of the License, or
8 : * (at your option) any later version.
9 : *
10 : * This program is distributed in the hope that it will be useful,
11 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 : * GNU General Public License for more details.
14 : *
15 : * You should have received a copy of the GNU General Public License
16 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 : */
18 :
19 : #include <cppunit/TestAssert.h>
20 : #include <cppunit/TestFixture.h>
21 : #include <cppunit/extensions/HelperMacros.h>
22 :
23 : #include <condition_variable>
24 : #include <msgpack.hpp>
25 : #include <filesystem>
26 :
27 : #include "../../test_runner.h"
28 : #include "account_const.h"
29 : #include "common.h"
30 : #include "conversation_interface.h"
31 : #include "fileutils.h"
32 : #include "jami.h"
33 : #include "jamidht/conversation.h"
34 : #include "jamidht/jamiaccount.h"
35 : #include "jamidht/swarm/swarm_channel_handler.h"
36 : #include "manager.h"
37 :
38 : using namespace std::string_literals;
39 : using namespace std::literals::chrono_literals;
40 : using namespace libjami::Account;
41 :
42 : namespace jami {
43 :
44 : struct ConvInfoTest
45 : {
46 : std::string accountId;
47 : std::string convId;
48 : bool requestReceived = false;
49 : bool conversationReady = false;
50 : std::vector<std::map<std::string, std::string>> messages;
51 : Conversation::BootstrapStatus bootstrap {Conversation::BootstrapStatus::FAILED};
52 : };
53 :
54 : namespace test {
55 :
56 : class BootstrapTest : public CppUnit::TestFixture
57 : {
58 : public:
59 8 : ~BootstrapTest() { libjami::fini(); }
60 2 : static std::string name() { return "Bootstrap"; }
61 : void setUp();
62 : void tearDown();
63 :
64 : ConvInfoTest aliceData;
65 : ConvInfoTest bobData;
66 : ConvInfoTest bob2Data;
67 : ConvInfoTest carlaData;
68 :
69 : std::mutex mtx;
70 : std::condition_variable cv;
71 :
72 : private:
73 : void connectSignals();
74 :
75 : void testBootstrapOk();
76 : void testBootstrapFailed();
77 : void testBootstrapNeverNewDevice();
78 : void testBootstrapCompat();
79 :
80 2 : CPPUNIT_TEST_SUITE(BootstrapTest);
81 1 : CPPUNIT_TEST(testBootstrapOk);
82 1 : CPPUNIT_TEST(testBootstrapFailed);
83 1 : CPPUNIT_TEST(testBootstrapNeverNewDevice);
84 1 : CPPUNIT_TEST(testBootstrapCompat);
85 4 : CPPUNIT_TEST_SUITE_END();
86 : };
87 :
88 : CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(BootstrapTest, BootstrapTest::name());
89 :
90 : void
91 4 : BootstrapTest::setUp()
92 : {
93 4 : aliceData = {};
94 4 : bobData = {};
95 4 : bob2Data = {};
96 4 : carlaData = {};
97 :
98 : // Init daemon
99 4 : libjami::init(
100 : libjami::InitFlag(libjami::LIBJAMI_FLAG_DEBUG | libjami::LIBJAMI_FLAG_CONSOLE_LOG));
101 4 : if (not Manager::instance().initialized)
102 1 : CPPUNIT_ASSERT(libjami::start("jami-sample.yml"));
103 :
104 4 : auto actors = load_actors("actors/alice-bob-carla.yml");
105 4 : aliceData.accountId = actors["alice"];
106 4 : bobData.accountId = actors["bob"];
107 4 : carlaData.accountId = actors["carla"];
108 :
109 4 : Manager::instance().sendRegister(carlaData.accountId, false);
110 12 : wait_for_announcement_of({aliceData.accountId, bobData.accountId});
111 4 : connectSignals();
112 4 : }
113 :
114 : void
115 4 : BootstrapTest::tearDown()
116 : {
117 4 : libjami::unregisterSignalHandlers();
118 8 : auto bobArchive = std::filesystem::current_path().string() + "/bob.gz";
119 4 : std::remove(bobArchive.c_str());
120 :
121 4 : if (bob2Data.accountId.empty()) {
122 12 : wait_for_removal_of({aliceData.accountId, bobData.accountId, carlaData.accountId});
123 : } else {
124 6 : wait_for_removal_of(
125 1 : {aliceData.accountId, bobData.accountId, carlaData.accountId, bob2Data.accountId});
126 : }
127 4 : }
128 :
129 : void
130 4 : BootstrapTest::connectSignals()
131 : {
132 4 : std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> confHandlers;
133 4 : confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::ConversationReady>(
134 9 : [&](const std::string& accountId, const std::string& convId) {
135 9 : if (accountId == aliceData.accountId) {
136 4 : aliceData.convId = convId;
137 4 : aliceData.conversationReady = true;
138 5 : } else if (accountId == bobData.accountId) {
139 4 : bobData.convId = convId;
140 4 : bobData.conversationReady = true;
141 1 : } else if (accountId == bob2Data.accountId) {
142 1 : bob2Data.convId = convId;
143 1 : bob2Data.conversationReady = true;
144 0 : } else if (accountId == carlaData.accountId) {
145 0 : carlaData.convId = convId;
146 0 : carlaData.conversationReady = true;
147 : }
148 9 : cv.notify_one();
149 9 : }));
150 4 : confHandlers.insert(
151 8 : libjami::exportable_callback<libjami::ConversationSignal::ConversationRequestReceived>(
152 4 : [&](const std::string& accountId,
153 : const std::string& /* conversationId */,
154 : std::map<std::string, std::string> /*metadatas*/) {
155 4 : if (accountId == aliceData.accountId) {
156 0 : aliceData.requestReceived = true;
157 4 : } else if (accountId == bobData.accountId) {
158 4 : bobData.requestReceived = true;
159 0 : } else if (accountId == bob2Data.accountId) {
160 0 : bob2Data.requestReceived = true;
161 0 : } else if (accountId == carlaData.accountId) {
162 0 : carlaData.requestReceived = true;
163 : }
164 4 : cv.notify_one();
165 4 : }));
166 4 : confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::MessageReceived>(
167 10 : [&](const std::string& accountId,
168 : const std::string& /*conversationId*/,
169 : std::map<std::string, std::string> message) {
170 10 : if (accountId == aliceData.accountId) {
171 9 : aliceData.messages.emplace_back(message);
172 1 : } else if (accountId == bobData.accountId) {
173 1 : bobData.messages.emplace_back(message);
174 0 : } else if (accountId == bob2Data.accountId) {
175 0 : bob2Data.messages.emplace_back(message);
176 0 : } else if (accountId == carlaData.accountId) {
177 0 : carlaData.messages.emplace_back(message);
178 : }
179 10 : cv.notify_one();
180 10 : }));
181 4 : libjami::registerSignalHandlers(confHandlers);
182 :
183 : // Link callback for convModule()
184 4 : auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceData.accountId);
185 4 : auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobData.accountId);
186 4 : auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaData.accountId);
187 :
188 4 : aliceAccount->convModule()->onBootstrapStatus(
189 2 : [&](std::string /*convId*/, Conversation::BootstrapStatus status) {
190 2 : aliceData.bootstrap = status;
191 2 : cv.notify_one();
192 2 : });
193 4 : bobAccount->convModule()->onBootstrapStatus(
194 6 : [&](std::string /*convId*/, Conversation::BootstrapStatus status) {
195 6 : bobData.bootstrap = status;
196 6 : cv.notify_one();
197 6 : });
198 4 : carlaAccount->convModule()->onBootstrapStatus(
199 0 : [&](std::string /*convId*/, Conversation::BootstrapStatus status) {
200 0 : carlaData.bootstrap = status;
201 0 : cv.notify_one();
202 0 : });
203 4 : }
204 :
205 : void
206 1 : BootstrapTest::testBootstrapOk()
207 : {
208 1 : auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceData.accountId);
209 1 : auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobData.accountId);
210 1 : auto bobUri = bobAccount->getUsername();
211 :
212 1 : aliceAccount->convModule()->onBootstrapStatus(
213 3 : [&](std::string /*convId*/, Conversation::BootstrapStatus status) {
214 3 : aliceData.bootstrap = status;
215 3 : cv.notify_one();
216 3 : });
217 :
218 1 : std::unique_lock lk {mtx};
219 1 : auto convId = libjami::startConversation(aliceData.accountId);
220 :
221 1 : libjami::addConversationMember(aliceData.accountId, convId, bobUri);
222 6 : CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return bobData.requestReceived; }));
223 :
224 1 : auto aliceMsgSize = aliceData.messages.size();
225 1 : libjami::acceptConversationRequest(bobData.accountId, convId);
226 6 : CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() {
227 : return bobData.conversationReady && aliceData.messages.size() == aliceMsgSize + 1
228 : && aliceData.bootstrap == Conversation::BootstrapStatus::SUCCESS
229 : && bobData.bootstrap == Conversation::BootstrapStatus::SUCCESS;
230 : }));
231 1 : }
232 :
233 : void
234 1 : BootstrapTest::testBootstrapFailed()
235 : {
236 1 : auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceData.accountId);
237 1 : auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobData.accountId);
238 1 : auto bobUri = bobAccount->getUsername();
239 :
240 1 : aliceAccount->convModule()->onBootstrapStatus(
241 5 : [&](std::string /*convId*/, Conversation::BootstrapStatus status) {
242 5 : aliceData.bootstrap = status;
243 5 : cv.notify_one();
244 5 : });
245 :
246 1 : std::unique_lock lk {mtx};
247 1 : auto convId = libjami::startConversation(aliceData.accountId);
248 :
249 1 : libjami::addConversationMember(aliceData.accountId, convId, bobUri);
250 6 : CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return bobData.requestReceived; }));
251 :
252 1 : auto aliceMsgSize = aliceData.messages.size();
253 1 : libjami::acceptConversationRequest(bobData.accountId, convId);
254 6 : CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() {
255 : return bobData.conversationReady && aliceData.messages.size() == aliceMsgSize + 1
256 : && aliceData.bootstrap == Conversation::BootstrapStatus::SUCCESS
257 : && bobData.bootstrap == Conversation::BootstrapStatus::SUCCESS;
258 : }));
259 :
260 : // Now bob goes offline, it should disconnect alice
261 1 : Manager::instance().sendRegister(bobData.accountId, false);
262 : // Alice will try to maintain before failing (so will take 30secs to fail)
263 4 : CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&]() {
264 : return aliceData.bootstrap == Conversation::BootstrapStatus::FAILED;
265 : }));
266 1 : }
267 :
268 : void
269 1 : BootstrapTest::testBootstrapNeverNewDevice()
270 : {
271 1 : auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceData.accountId);
272 1 : auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobData.accountId);
273 1 : auto bobUri = bobAccount->getUsername();
274 :
275 1 : aliceAccount->convModule()->onBootstrapStatus(
276 3 : [&](std::string /*convId*/, Conversation::BootstrapStatus status) {
277 3 : aliceData.bootstrap = status;
278 3 : cv.notify_one();
279 3 : });
280 :
281 1 : std::unique_lock lk {mtx};
282 1 : auto convId = libjami::startConversation(aliceData.accountId);
283 :
284 1 : libjami::addConversationMember(aliceData.accountId, convId, bobUri);
285 6 : CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return bobData.requestReceived; }));
286 :
287 1 : auto aliceMsgSize = aliceData.messages.size();
288 1 : libjami::acceptConversationRequest(bobData.accountId, convId);
289 5 : CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() {
290 : return bobData.conversationReady && aliceData.messages.size() == aliceMsgSize + 1
291 : && aliceData.bootstrap == Conversation::BootstrapStatus::SUCCESS
292 : && bobData.bootstrap == Conversation::BootstrapStatus::SUCCESS;
293 : }));
294 :
295 : // Alice offline
296 1 : Manager::instance().sendRegister(aliceData.accountId, false);
297 : // Bob will try to maintain before failing (so will take 30secs to fail)
298 4 : CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&]() {
299 : return bobData.bootstrap == Conversation::BootstrapStatus::FAILED;
300 : }));
301 :
302 : // Create bob2
303 2 : auto bobArchive = std::filesystem::current_path().string() + "/bob.gz";
304 1 : std::remove(bobArchive.c_str());
305 1 : bobAccount->exportArchive(bobArchive);
306 2 : std::map<std::string, std::string> details = libjami::getAccountTemplate("RING");
307 1 : details[ConfProperties::TYPE] = "RING";
308 1 : details[ConfProperties::DISPLAYNAME] = "BOB2";
309 1 : details[ConfProperties::ALIAS] = "BOB2";
310 1 : details[ConfProperties::UPNP_ENABLED] = "true";
311 1 : details[ConfProperties::ARCHIVE_PASSWORD] = "";
312 1 : details[ConfProperties::ARCHIVE_PIN] = "";
313 1 : details[ConfProperties::ARCHIVE_PATH] = bobArchive;
314 1 : bob2Data.accountId = Manager::instance().addAccount(details);
315 1 : auto bob2Account = Manager::instance().getAccount<JamiAccount>(bob2Data.accountId);
316 :
317 1 : std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> confHandlers;
318 1 : bool bob2Connected = false;
319 1 : confHandlers.insert(
320 2 : libjami::exportable_callback<libjami::ConfigurationSignal::VolatileDetailsChanged>(
321 6 : [&](const std::string&, const std::map<std::string, std::string>&) {
322 6 : auto details = bob2Account->getVolatileAccountDetails();
323 12 : auto daemonStatus = details[libjami::Account::ConfProperties::Registration::STATUS];
324 6 : if (daemonStatus != "UNREGISTERED")
325 5 : bob2Connected = true;
326 6 : cv.notify_one();
327 6 : }));
328 1 : libjami::registerSignalHandlers(confHandlers);
329 :
330 4 : CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return bob2Connected; }));
331 1 : bob2Account->convModule()->onBootstrapStatus(
332 2 : [&](std::string /*convId*/, Conversation::BootstrapStatus status) {
333 2 : bob2Data.bootstrap = status;
334 2 : cv.notify_one();
335 2 : });
336 :
337 : // Disconnect bob2, to create a valid conv betwen Alice and Bob1
338 6 : CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() {
339 : return bobData.bootstrap == Conversation::BootstrapStatus::SUCCESS
340 : && bob2Data.bootstrap == Conversation::BootstrapStatus::SUCCESS;
341 : }));
342 :
343 : // Bob offline
344 1 : Manager::instance().sendRegister(bobData.accountId, false);
345 : // Bob2 will try to maintain before failing (so will take 30secs to fail)
346 5 : CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&]() {
347 : return bob2Data.bootstrap == Conversation::BootstrapStatus::FAILED;
348 : }));
349 :
350 : // Alice bootstrap should go to fallback (because bob2 never wrote into the conversation) & Connected
351 1 : Manager::instance().sendRegister(aliceData.accountId, true);
352 : // Wait for announcement, ICE fallback + delay
353 2 : CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&]() {
354 : return aliceData.bootstrap == Conversation::BootstrapStatus::SUCCESS;
355 : }));
356 1 : }
357 :
358 : void
359 1 : BootstrapTest::testBootstrapCompat()
360 : {
361 1 : auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceData.accountId);
362 1 : auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobData.accountId);
363 1 : auto bobUri = bobAccount->getUsername();
364 :
365 1 : dynamic_cast<SwarmChannelHandler*>(aliceAccount->channelHandlers()[Uri::Scheme::SWARM].get())
366 : ->disableSwarmManager
367 1 : = true;
368 :
369 1 : std::unique_lock lk {mtx};
370 1 : auto convId = libjami::startConversation(aliceData.accountId);
371 :
372 1 : libjami::addConversationMember(aliceData.accountId, convId, bobUri);
373 6 : CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return bobData.requestReceived; }));
374 :
375 1 : auto aliceMsgSize = aliceData.messages.size();
376 1 : libjami::acceptConversationRequest(bobData.accountId, convId);
377 4 : CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() {
378 : return bobData.conversationReady && aliceData.messages.size() == aliceMsgSize + 1;
379 : }));
380 :
381 1 : auto bobMsgSize = bobData.messages.size();
382 1 : libjami::sendMessage(aliceData.accountId, convId, "hi"s, "");
383 1 : cv.wait_for(lk, 30s, [&]() {
384 3 : return bobData.messages.size() == bobMsgSize + 1
385 3 : && bobData.bootstrap == Conversation::BootstrapStatus::FAILED;
386 : });
387 1 : }
388 :
389 : } // namespace test
390 : } // namespace jami
391 :
392 1 : RING_TEST_RUNNER(jami::test::BootstrapTest::name())
|