Line data Source code
1 : /*
2 : * Copyright (C) 2022-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 <string>
25 : #include <filesystem>
26 : #include <fstream>
27 : #include <streambuf>
28 : #include <fmt/format.h>
29 :
30 : #include "manager.h"
31 : #include "jamidht/accountarchive.h"
32 : #include "jamidht/jamiaccount.h"
33 : #include "../../test_runner.h"
34 : #include "account_const.h"
35 : #include "common.h"
36 : #include "fileutils.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 : namespace test {
44 :
45 : class MigrationTest : public CppUnit::TestFixture
46 : {
47 : public:
48 3 : MigrationTest()
49 3 : {
50 : // Init daemon
51 3 : libjami::init(libjami::InitFlag(libjami::LIBJAMI_FLAG_DEBUG | libjami::LIBJAMI_FLAG_CONSOLE_LOG));
52 3 : if (not Manager::instance().initialized)
53 1 : CPPUNIT_ASSERT(libjami::start("dring-sample.yml"));
54 3 : }
55 6 : ~MigrationTest() { libjami::fini(); }
56 2 : static std::string name() { return "AccountArchive"; }
57 : void setUp();
58 : void tearDown();
59 :
60 : std::string aliceId;
61 : std::string bobId;
62 : std::string bob2Id;
63 :
64 : private:
65 : void testLoadExpiredAccount();
66 : void testMigrationAfterRevokation();
67 : void testExpiredDeviceInSwarm();
68 :
69 2 : CPPUNIT_TEST_SUITE(MigrationTest);
70 1 : CPPUNIT_TEST(testLoadExpiredAccount);
71 1 : CPPUNIT_TEST(testMigrationAfterRevokation);
72 1 : CPPUNIT_TEST(testExpiredDeviceInSwarm);
73 4 : CPPUNIT_TEST_SUITE_END();
74 : };
75 :
76 : CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(MigrationTest, MigrationTest::name());
77 :
78 : void
79 3 : MigrationTest::setUp()
80 : {
81 3 : auto actors = load_actors("actors/alice-bob.yml");
82 3 : aliceId = actors["alice"];
83 3 : bobId = actors["bob"];
84 3 : }
85 :
86 : void
87 3 : MigrationTest::tearDown()
88 : {
89 6 : auto bobArchive = std::filesystem::current_path().string() + "/bob.gz";
90 3 : std::remove(bobArchive.c_str());
91 :
92 3 : if (!bob2Id.empty())
93 4 : wait_for_removal_of({aliceId, bob2Id, bobId});
94 : else
95 6 : wait_for_removal_of({aliceId, bobId});
96 3 : }
97 :
98 : void
99 1 : MigrationTest::testLoadExpiredAccount()
100 : {
101 3 : wait_for_announcement_of({aliceId, bobId});
102 1 : auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
103 1 : auto aliceUri = aliceAccount->getUsername();
104 1 : auto aliceDevice = std::string(aliceAccount->currentDeviceId());
105 :
106 : // Get alice's expiration
107 2 : auto archivePath = fileutils::get_data_dir() / aliceAccount->getAccountID() / "archive.gz";
108 2 : auto devicePath = fileutils::get_data_dir() / aliceAccount->getAccountID() / "ring_device.crt";
109 1 : auto archive = AccountArchive(archivePath);
110 2 : auto deviceCert = dht::crypto::Certificate(fileutils::loadFile(devicePath));
111 1 : auto deviceExpiration = deviceCert.getExpiration();
112 1 : auto accountExpiration = archive.id.second->getExpiration();
113 :
114 : // Update validity
115 1 : CPPUNIT_ASSERT(aliceAccount->setValidity("", "", {}, 9));
116 1 : archive = AccountArchive(archivePath);
117 1 : deviceCert = dht::crypto::Certificate(fileutils::loadFile(devicePath));
118 1 : auto newDeviceExpiration = deviceCert.getExpiration();
119 1 : auto newAccountExpiration = archive.id.second->getExpiration();
120 :
121 : // Check expiration is changed
122 1 : CPPUNIT_ASSERT(newDeviceExpiration < deviceExpiration
123 : && newAccountExpiration < accountExpiration);
124 :
125 : // Sleep and wait that certificate is expired
126 1 : std::this_thread::sleep_for(10s);
127 :
128 : // reload account, check migration signals
129 1 : std::mutex mtx;
130 1 : std::unique_lock lk {mtx};
131 1 : std::condition_variable cv;
132 1 : std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> confHandlers;
133 1 : auto aliceMigrated = false;
134 1 : confHandlers.insert(libjami::exportable_callback<libjami::ConfigurationSignal::MigrationEnded>(
135 1 : [&](const std::string& accountId, const std::string& state) {
136 1 : if (accountId == aliceId && state == "SUCCESS") {
137 1 : aliceMigrated = true;
138 : }
139 1 : cv.notify_one();
140 1 : }));
141 1 : libjami::registerSignalHandlers(confHandlers);
142 :
143 : // Check migration is triggered and expiration updated
144 1 : aliceAccount->forceReloadAccount();
145 3 : CPPUNIT_ASSERT(cv.wait_for(lk, 15s, [&]() { return aliceMigrated; }));
146 :
147 1 : archive = AccountArchive(archivePath);
148 1 : deviceCert = dht::crypto::Certificate(fileutils::loadFile(devicePath));
149 1 : deviceExpiration = deviceCert.getExpiration();
150 1 : accountExpiration = archive.id.second->getExpiration();
151 1 : CPPUNIT_ASSERT(newDeviceExpiration < deviceExpiration
152 : && newAccountExpiration < accountExpiration);
153 1 : CPPUNIT_ASSERT(aliceUri == aliceAccount->getUsername());
154 1 : CPPUNIT_ASSERT(aliceDevice == aliceAccount->currentDeviceId());
155 1 : }
156 :
157 : void
158 1 : MigrationTest::testMigrationAfterRevokation()
159 : {
160 3 : wait_for_announcement_of({aliceId, bobId});
161 1 : auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
162 1 : auto bobUri = bobAccount->getUsername();
163 :
164 : // Generate bob2
165 1 : std::mutex mtx;
166 1 : std::unique_lock lk {mtx};
167 1 : std::condition_variable cv;
168 :
169 : // Add second device for Bob
170 1 : std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> confHandlers;
171 2 : auto bobArchive = std::filesystem::current_path().string() + "/bob.gz";
172 1 : std::remove(bobArchive.c_str());
173 1 : bobAccount->exportArchive(bobArchive);
174 :
175 2 : std::map<std::string, std::string> details = libjami::getAccountTemplate("RING");
176 1 : details[ConfProperties::TYPE] = "RING";
177 1 : details[ConfProperties::DISPLAYNAME] = "BOB2";
178 1 : details[ConfProperties::ALIAS] = "BOB2";
179 1 : details[ConfProperties::UPNP_ENABLED] = "true";
180 1 : details[ConfProperties::ARCHIVE_PASSWORD] = "";
181 1 : details[ConfProperties::ARCHIVE_PIN] = "";
182 1 : details[ConfProperties::ARCHIVE_PATH] = bobArchive;
183 :
184 1 : auto deviceRevoked = false;
185 1 : confHandlers.insert(
186 2 : libjami::exportable_callback<libjami::ConfigurationSignal::DeviceRevocationEnded>(
187 1 : [&](const std::string& accountId, const std::string&, int status) {
188 1 : if (accountId == bobId && status == 0)
189 1 : deviceRevoked = true;
190 1 : cv.notify_one();
191 1 : }));
192 1 : auto bobMigrated = false;
193 1 : confHandlers.insert(libjami::exportable_callback<libjami::ConfigurationSignal::MigrationEnded>(
194 1 : [&](const std::string& accountId, const std::string& state) {
195 1 : if (accountId == bob2Id && state == "SUCCESS") {
196 1 : bobMigrated = true;
197 : }
198 1 : cv.notify_one();
199 1 : }));
200 1 : auto knownChanged = false;
201 1 : confHandlers.insert(libjami::exportable_callback<libjami::ConfigurationSignal::KnownDevicesChanged>(
202 6 : [&](const std::string& accountId, auto devices) {
203 6 : if (accountId == bobId && devices.size() == 2)
204 2 : knownChanged = true;
205 6 : cv.notify_one();
206 6 : }));
207 1 : libjami::registerSignalHandlers(confHandlers);
208 :
209 1 : bob2Id = Manager::instance().addAccount(details);
210 1 : auto bob2Account = Manager::instance().getAccount<JamiAccount>(bob2Id);
211 :
212 5 : CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&]() { return knownChanged; }));
213 :
214 : // Revoke bob2
215 1 : auto bob2Device = std::string(bob2Account->currentDeviceId());
216 1 : bobAccount->revokeDevice(bob2Device);
217 2 : CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&]() { return deviceRevoked; }));
218 : // Note: bob2 will need some seconds to get the revokation list
219 1 : std::this_thread::sleep_for(10s);
220 :
221 : // Check migration is triggered and expiration updated
222 1 : bob2Account->forceReloadAccount();
223 4 : CPPUNIT_ASSERT(cv.wait_for(lk, 15s, [&]() { return bobMigrated; }));
224 : // Because the device was revoked, a new ID must be generated there
225 1 : CPPUNIT_ASSERT(bob2Device != bob2Account->currentDeviceId());
226 1 : }
227 :
228 : void
229 1 : MigrationTest::testExpiredDeviceInSwarm()
230 : {
231 1 : auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
232 :
233 1 : std::mutex mtx;
234 1 : std::unique_lock lk {mtx};
235 1 : std::condition_variable cv;
236 1 : std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> confHandlers;
237 1 : auto messageBobReceived = 0, messageAliceReceived = 0;
238 1 : confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::MessageReceived>(
239 8 : [&](const std::string& accountId,
240 : const std::string& /* conversationId */,
241 : std::map<std::string, std::string> /*message*/) {
242 8 : if (accountId == bobId) {
243 3 : messageBobReceived += 1;
244 : } else {
245 5 : messageAliceReceived += 1;
246 : }
247 8 : cv.notify_one();
248 8 : }));
249 1 : bool requestReceived = false;
250 1 : confHandlers.insert(
251 2 : libjami::exportable_callback<libjami::ConversationSignal::ConversationRequestReceived>(
252 1 : [&](const std::string& /*accountId*/,
253 : const std::string& /* conversationId */,
254 : std::map<std::string, std::string> /*metadatas*/) {
255 1 : requestReceived = true;
256 1 : cv.notify_one();
257 1 : }));
258 1 : bool conversationReady = false;
259 1 : confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::ConversationReady>(
260 2 : [&](const std::string& accountId, const std::string& /* conversationId */) {
261 2 : if (accountId == bobId) {
262 1 : conversationReady = true;
263 1 : cv.notify_one();
264 : }
265 2 : }));
266 1 : auto aliceMigrated = false;
267 1 : confHandlers.insert(libjami::exportable_callback<libjami::ConfigurationSignal::MigrationEnded>(
268 2 : [&](const std::string& accountId, const std::string& state) {
269 2 : if (accountId == aliceId && state == "SUCCESS") {
270 2 : aliceMigrated = true;
271 : }
272 2 : cv.notify_one();
273 2 : }));
274 1 : bool aliceStopped = false, aliceAnnounced = false, aliceRegistered = false, aliceRegistering = false;
275 1 : confHandlers.insert(
276 2 : libjami::exportable_callback<libjami::ConfigurationSignal::VolatileDetailsChanged>(
277 20 : [&](const std::string&, const std::map<std::string, std::string>&) {
278 20 : auto details = aliceAccount->getVolatileAccountDetails();
279 40 : auto daemonStatus = details[libjami::Account::ConfProperties::Registration::STATUS];
280 20 : if (daemonStatus == "UNREGISTERED")
281 3 : aliceStopped = true;
282 17 : else if (daemonStatus == "REGISTERED")
283 10 : aliceRegistered = true;
284 7 : else if (daemonStatus == "TRYING")
285 5 : aliceRegistering = true;
286 40 : auto announced = details[libjami::Account::VolatileProperties::DEVICE_ANNOUNCED];
287 20 : if (announced == "true")
288 2 : aliceAnnounced = true;
289 20 : cv.notify_one();
290 20 : }));
291 1 : libjami::registerSignalHandlers(confHandlers);
292 :
293 : // NOTE: We must update certificate before announcing, else, there will be several
294 : // certificates on the DHT
295 :
296 4 : CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&]() { return aliceRegistering; }));
297 1 : auto aliceDevice = std::string(aliceAccount->currentDeviceId());
298 1 : CPPUNIT_ASSERT(aliceAccount->setValidity("", "", {}, 90));
299 1 : auto now = std::chrono::system_clock::now();
300 1 : aliceRegistered = false;
301 1 : aliceAccount->forceReloadAccount();
302 6 : CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&]() { return aliceRegistered; }));
303 1 : CPPUNIT_ASSERT(aliceAccount->currentDeviceId() == aliceDevice);
304 :
305 1 : aliceStopped = false;
306 1 : Manager::instance().sendRegister(aliceId, false);
307 3 : CPPUNIT_ASSERT(cv.wait_for(lk, 15s, [&]() { return aliceStopped; }));
308 1 : aliceAnnounced = false;
309 1 : Manager::instance().sendRegister(aliceId, true);
310 8 : CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&]() { return aliceAnnounced; }));
311 :
312 1 : CPPUNIT_ASSERT(aliceAccount->currentDeviceId() == aliceDevice);
313 :
314 : // Create conversation
315 1 : auto convId = libjami::startConversation(aliceId);
316 :
317 1 : auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
318 1 : auto bobUri = bobAccount->getUsername();
319 1 : libjami::addConversationMember(aliceId, convId, bobUri);
320 4 : CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&]() { return requestReceived; }));
321 :
322 1 : messageAliceReceived = 0;
323 1 : libjami::acceptConversationRequest(bobId, convId);
324 3 : CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&]() { return conversationReady; }));
325 :
326 : // Assert that repository exists
327 2 : auto repoPath = fileutils::get_data_dir() / bobAccount->getAccountID()
328 4 : / "conversations" / convId;
329 1 : CPPUNIT_ASSERT(std::filesystem::is_directory(repoPath));
330 : // Wait that alice sees Bob
331 3 : cv.wait_for(lk, 20s, [&]() { return messageAliceReceived == 1; });
332 :
333 1 : messageBobReceived = 0;
334 1 : libjami::sendMessage(aliceId, convId, "hi"s, "");
335 4 : CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&]() { return messageBobReceived == 1; }));
336 :
337 : // Wait for certificate to expire
338 1 : std::this_thread::sleep_until(now + 100s);
339 : // Check migration is triggered and expiration updated
340 1 : aliceAnnounced = false;
341 1 : aliceAccount->forceReloadAccount();
342 8 : CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return aliceAnnounced; }));
343 1 : CPPUNIT_ASSERT(aliceAccount->currentDeviceId() == aliceDevice);
344 :
345 : // check that certificate in conversation is expired
346 3 : auto devicePath = repoPath / "devices" / fmt::format("{}.crt", aliceAccount->currentDeviceId());
347 1 : CPPUNIT_ASSERT(std::filesystem::is_regular_file(devicePath));
348 2 : auto cert = dht::crypto::Certificate(fileutils::loadFile(devicePath));
349 1 : now = std::chrono::system_clock::now();
350 1 : CPPUNIT_ASSERT(cert.getExpiration() < now);
351 :
352 : // Resend a new message
353 1 : messageBobReceived = 0;
354 1 : libjami::sendMessage(aliceId, convId, "hi again"s, "");
355 4 : CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&]() { return messageBobReceived == 1; }));
356 1 : messageAliceReceived = 0;
357 1 : libjami::sendMessage(bobId, convId, "hi!"s, "");
358 4 : CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&]() { return messageAliceReceived == 1; }));
359 :
360 : // check that certificate in conversation is updated
361 1 : CPPUNIT_ASSERT(std::filesystem::is_regular_file(devicePath));
362 1 : cert = dht::crypto::Certificate(fileutils::loadFile(devicePath));
363 1 : now = std::chrono::system_clock::now();
364 1 : CPPUNIT_ASSERT(cert.getExpiration() > now);
365 :
366 : // Check same device as begining
367 1 : CPPUNIT_ASSERT(aliceAccount->currentDeviceId() == aliceDevice);
368 1 : }
369 :
370 : } // namespace test
371 : } // namespace jami
372 :
373 1 : RING_TEST_RUNNER(jami::test::MigrationTest::name())
|