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