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