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 "manager.h"
19 : #include "jamidht/conversationrepository.h"
20 : #include "jamidht/gitserver.h"
21 : #include "jamidht/jamiaccount.h"
22 : #include "../../test_runner.h"
23 : #include "jami.h"
24 : #include "base64.h"
25 : #include "fileutils.h"
26 : #include "account_const.h"
27 : #include "common.h"
28 :
29 : #include <git2.h>
30 :
31 : #include <dhtnet/connectionmanager.h>
32 :
33 : #include <cppunit/TestAssert.h>
34 : #include <cppunit/TestFixture.h>
35 : #include <cppunit/extensions/HelperMacros.h>
36 :
37 : #include <condition_variable>
38 : #include <string>
39 : #include <fstream>
40 : #include <streambuf>
41 : #include <filesystem>
42 :
43 : using namespace std::string_literals;
44 : using namespace libjami::Account;
45 :
46 : namespace jami {
47 : namespace test {
48 :
49 : class ConversationRepositoryTest : public CppUnit::TestFixture
50 : {
51 : public:
52 7 : ConversationRepositoryTest()
53 7 : {
54 : // Init daemon
55 7 : libjami::init(
56 : libjami::InitFlag(libjami::LIBJAMI_FLAG_DEBUG | libjami::LIBJAMI_FLAG_CONSOLE_LOG));
57 7 : if (not Manager::instance().initialized)
58 1 : CPPUNIT_ASSERT(libjami::start("jami-sample.yml"));
59 7 : }
60 14 : ~ConversationRepositoryTest() { libjami::fini(); }
61 2 : static std::string name() { return "ConversationRepository"; }
62 : void setUp();
63 : void tearDown();
64 :
65 : std::string aliceId;
66 : std::string bobId;
67 :
68 : private:
69 : void testCreateRepository();
70 : void testAddSomeMessages();
71 : void testLogMessages();
72 : void testMerge();
73 : void testFFMerge();
74 : void testDiff();
75 :
76 : void testMergeProfileWithConflict();
77 :
78 : std::string addCommit(git_repository* repo,
79 : const std::shared_ptr<JamiAccount> account,
80 : const std::string& branch,
81 : const std::string& commit_msg);
82 : void addAll(git_repository* repo);
83 : bool merge_in_main(const std::shared_ptr<JamiAccount> account,
84 : git_repository* repo,
85 : const std::string& commit_ref);
86 :
87 2 : CPPUNIT_TEST_SUITE(ConversationRepositoryTest);
88 1 : CPPUNIT_TEST(testCreateRepository);
89 1 : CPPUNIT_TEST(testAddSomeMessages);
90 1 : CPPUNIT_TEST(testLogMessages);
91 1 : CPPUNIT_TEST(testMerge);
92 1 : CPPUNIT_TEST(testFFMerge);
93 1 : CPPUNIT_TEST(testDiff);
94 1 : CPPUNIT_TEST(testMergeProfileWithConflict);
95 :
96 4 : CPPUNIT_TEST_SUITE_END();
97 : };
98 :
99 : CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(ConversationRepositoryTest,
100 : ConversationRepositoryTest::name());
101 :
102 : void
103 7 : ConversationRepositoryTest::setUp()
104 : {
105 14 : auto actors = load_actors_and_wait_for_announcement("actors/alice-bob.yml");
106 7 : aliceId = actors["alice"];
107 7 : bobId = actors["bob"];
108 7 : }
109 :
110 : void
111 7 : ConversationRepositoryTest::tearDown()
112 : {
113 21 : wait_for_removal_of({aliceId, bobId});
114 7 : }
115 :
116 : void
117 1 : ConversationRepositoryTest::testCreateRepository()
118 : {
119 1 : auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
120 1 : auto aliceDeviceId = DeviceId(std::string(aliceAccount->currentDeviceId()));
121 1 : auto uri = aliceAccount->getUsername();
122 :
123 2 : auto repository = ConversationRepository::createConversation(aliceAccount);
124 :
125 : // Assert that repository exists
126 1 : CPPUNIT_ASSERT(repository != nullptr);
127 2 : auto repoPath = fileutils::get_data_dir() / aliceAccount->getAccountID()
128 4 : / "conversations" / repository->id();
129 1 : CPPUNIT_ASSERT(std::filesystem::is_directory(repoPath));
130 :
131 : // Assert that first commit is signed by alice
132 : git_repository* repo;
133 1 : CPPUNIT_ASSERT(git_repository_open(&repo, repoPath.c_str()) == 0);
134 :
135 : // 1. Verify that last commit is correctly signed by alice
136 : git_oid commit_id;
137 1 : CPPUNIT_ASSERT(git_reference_name_to_id(&commit_id, repo, "HEAD") == 0);
138 :
139 1 : git_buf signature = {}, signed_data = {};
140 1 : git_commit_extract_signature(&signature, &signed_data, repo, &commit_id, "signature");
141 2 : auto pk = base64::decode(std::string(signature.ptr, signature.ptr + signature.size));
142 1 : auto data = std::vector<uint8_t>(signed_data.ptr, signed_data.ptr + signed_data.size);
143 1 : git_repository_free(repo);
144 :
145 1 : CPPUNIT_ASSERT(aliceAccount->identity().second->getPublicKey().checkSignature(data, pk));
146 :
147 : // 2. Check created files
148 2 : auto CRLsPath = repoPath / "CRLs" / aliceDeviceId.toString();
149 1 : CPPUNIT_ASSERT(std::filesystem::is_directory(repoPath));
150 :
151 2 : auto adminCrt = repoPath / "admins" / (uri + ".crt");
152 1 : CPPUNIT_ASSERT(std::filesystem::is_regular_file(adminCrt));
153 :
154 1 : auto crt = std::ifstream(adminCrt);
155 1 : std::string adminCrtStr((std::istreambuf_iterator<char>(crt)), std::istreambuf_iterator<char>());
156 :
157 1 : auto cert = aliceAccount->identity().second;
158 1 : auto deviceCert = cert->toString(false);
159 1 : auto parentCert = cert->issuer->toString(true);
160 :
161 1 : CPPUNIT_ASSERT(adminCrtStr == parentCert);
162 :
163 2 : auto deviceCrt = repoPath / "devices" / (aliceDeviceId.toString() + ".crt");
164 1 : CPPUNIT_ASSERT(std::filesystem::is_regular_file(deviceCrt));
165 :
166 1 : crt = std::ifstream(deviceCrt);
167 : std::string deviceCrtStr((std::istreambuf_iterator<char>(crt)),
168 1 : std::istreambuf_iterator<char>());
169 :
170 1 : CPPUNIT_ASSERT(deviceCrtStr == deviceCert);
171 1 : }
172 :
173 : void
174 1 : ConversationRepositoryTest::testAddSomeMessages()
175 : {
176 1 : auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
177 2 : auto repository = ConversationRepository::createConversation(aliceAccount);
178 :
179 2 : auto id1 = repository->commitMessage("Commit 1");
180 2 : auto id2 = repository->commitMessage("Commit 2");
181 2 : auto id3 = repository->commitMessage("Commit 3");
182 :
183 1 : auto messages = repository->log();
184 1 : CPPUNIT_ASSERT(messages.size() == 4 /* 3 + initial */);
185 1 : CPPUNIT_ASSERT(messages[0].id == id3);
186 1 : CPPUNIT_ASSERT(messages[0].parents.front() == id2);
187 1 : CPPUNIT_ASSERT(messages[0].commit_msg == "Commit 3");
188 1 : CPPUNIT_ASSERT(messages[0].author.name == messages[3].author.name);
189 1 : CPPUNIT_ASSERT(messages[0].author.email == messages[3].author.email);
190 1 : CPPUNIT_ASSERT(messages[1].id == id2);
191 1 : CPPUNIT_ASSERT(messages[1].parents.front() == id1);
192 1 : CPPUNIT_ASSERT(messages[1].commit_msg == "Commit 2");
193 1 : CPPUNIT_ASSERT(messages[1].author.name == messages[3].author.name);
194 1 : CPPUNIT_ASSERT(messages[1].author.email == messages[3].author.email);
195 1 : CPPUNIT_ASSERT(messages[2].id == id1);
196 1 : CPPUNIT_ASSERT(messages[2].commit_msg == "Commit 1");
197 1 : CPPUNIT_ASSERT(messages[2].author.name == messages[3].author.name);
198 1 : CPPUNIT_ASSERT(messages[2].author.email == messages[3].author.email);
199 1 : CPPUNIT_ASSERT(messages[2].parents.front() == repository->id());
200 : // Check sig
201 1 : CPPUNIT_ASSERT(
202 : aliceAccount->identity().second->getPublicKey().checkSignature(messages[0].signed_content,
203 : messages[0].signature));
204 1 : CPPUNIT_ASSERT(
205 : aliceAccount->identity().second->getPublicKey().checkSignature(messages[1].signed_content,
206 : messages[1].signature));
207 1 : CPPUNIT_ASSERT(
208 : aliceAccount->identity().second->getPublicKey().checkSignature(messages[2].signed_content,
209 : messages[2].signature));
210 1 : }
211 :
212 : void
213 1 : ConversationRepositoryTest::testLogMessages()
214 : {
215 1 : auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
216 2 : auto repository = ConversationRepository::createConversation(aliceAccount);
217 :
218 2 : auto id1 = repository->commitMessage("Commit 1");
219 2 : auto id2 = repository->commitMessage("Commit 2");
220 2 : auto id3 = repository->commitMessage("Commit 3");
221 :
222 1 : LogOptions options;
223 1 : options.from = repository->id();
224 1 : options.nbOfCommits = 1;
225 1 : auto messages = repository->log(options);
226 1 : CPPUNIT_ASSERT(messages.size() == 1);
227 1 : CPPUNIT_ASSERT(messages[0].id == repository->id());
228 1 : options.from = id2;
229 1 : options.nbOfCommits = 2;
230 1 : messages = repository->log(options);
231 1 : CPPUNIT_ASSERT(messages.size() == 2);
232 1 : CPPUNIT_ASSERT(messages[0].id == id2);
233 1 : CPPUNIT_ASSERT(messages[1].id == id1);
234 1 : options.from = repository->id();
235 1 : options.nbOfCommits = 3;
236 1 : messages = repository->log(options);
237 1 : CPPUNIT_ASSERT(messages.size() == 1);
238 1 : CPPUNIT_ASSERT(messages[0].id == repository->id());
239 1 : }
240 :
241 : std::string
242 7 : ConversationRepositoryTest::addCommit(git_repository* repo,
243 : const std::shared_ptr<JamiAccount> account,
244 : const std::string& branch,
245 : const std::string& commit_msg)
246 : {
247 7 : auto deviceId = DeviceId(std::string(account->currentDeviceId()));
248 7 : auto name = account->getDisplayName();
249 7 : if (name.empty())
250 0 : name = deviceId.toString();
251 :
252 7 : git_signature* sig_ptr = nullptr;
253 : // Sign commit's buffer
254 7 : if (git_signature_new(&sig_ptr, name.c_str(), deviceId.to_c_str(), std::time(nullptr), 0) < 0) {
255 0 : JAMI_ERR("Unable to create a commit signature.");
256 0 : return {};
257 : }
258 7 : GitSignature sig {sig_ptr, git_signature_free};
259 :
260 : // Retrieve current HEAD
261 : git_oid commit_id;
262 7 : if (git_reference_name_to_id(&commit_id, repo, "HEAD") < 0) {
263 0 : JAMI_ERR("Unable to get reference for HEAD");
264 0 : return {};
265 : }
266 :
267 7 : git_commit* head_ptr = nullptr;
268 7 : if (git_commit_lookup(&head_ptr, repo, &commit_id) < 0) {
269 0 : JAMI_ERR("Unable to look up HEAD commit");
270 0 : return {};
271 : }
272 7 : GitCommit head_commit {head_ptr, git_commit_free};
273 :
274 : // Retrieve current index
275 7 : git_index* index_ptr = nullptr;
276 7 : if (git_repository_index(&index_ptr, repo) < 0) {
277 0 : JAMI_ERR("Unable to open repository index");
278 0 : return {};
279 : }
280 7 : GitIndex index {index_ptr, git_index_free};
281 :
282 : git_oid tree_id;
283 7 : if (git_index_write_tree(&tree_id, index.get()) < 0) {
284 0 : JAMI_ERR("Unable to write initial tree from index");
285 0 : return {};
286 : }
287 :
288 7 : git_tree* tree_ptr = nullptr;
289 7 : if (git_tree_lookup(&tree_ptr, repo, &tree_id) < 0) {
290 0 : JAMI_ERR("Unable to look up initial tree");
291 0 : return {};
292 : }
293 7 : GitTree tree = {tree_ptr, git_tree_free};
294 :
295 7 : git_buf to_sign = {};
296 : #if( LIBGIT2_VER_MAJOR > 1 ) || ( LIBGIT2_VER_MAJOR == 1 && LIBGIT2_VER_MINOR >= 8 )
297 : // For libgit2 version 1.8.0 and above
298 7 : git_commit* const head_ref[1] = {head_commit.get()};
299 : #else
300 : const git_commit* head_ref[1] = {head_commit.get()};
301 : #endif
302 14 : if (git_commit_create_buffer(&to_sign,
303 : repo,
304 7 : sig.get(),
305 7 : sig.get(),
306 : nullptr,
307 : commit_msg.c_str(),
308 7 : tree.get(),
309 : 1,
310 : &head_ref[0])
311 7 : < 0) {
312 0 : JAMI_ERR("Unable to create commit buffer");
313 0 : return {};
314 : }
315 :
316 : // git commit -S
317 7 : auto to_sign_vec = std::vector<uint8_t>(to_sign.ptr, to_sign.ptr + to_sign.size);
318 7 : auto signed_buf = account->identity().first->sign(to_sign_vec);
319 7 : std::string signed_str = base64::encode(signed_buf);
320 7 : if (git_commit_create_with_signature(&commit_id,
321 : repo,
322 7 : to_sign.ptr,
323 : signed_str.c_str(),
324 : "signature")
325 7 : < 0) {
326 0 : JAMI_ERR("Unable to sign commit");
327 0 : return {};
328 : }
329 :
330 7 : auto commit_str = git_oid_tostr_s(&commit_id);
331 7 : if (commit_str) {
332 7 : JAMI_INFO("New commit added with id: %s", commit_str);
333 : // Move commit to main branch
334 7 : git_reference* ref_ptr = nullptr;
335 7 : std::string branch_name = "refs/heads/" + branch;
336 7 : if (git_reference_create(&ref_ptr, repo, branch_name.c_str(), &commit_id, true, nullptr)
337 7 : < 0) {
338 0 : JAMI_WARN("Unable to move commit to main");
339 : }
340 7 : git_reference_free(ref_ptr);
341 7 : }
342 7 : return commit_str ? commit_str : "";
343 7 : }
344 :
345 : void
346 3 : ConversationRepositoryTest::addAll(git_repository* repo)
347 : {
348 : // git add -A
349 3 : git_index* index_ptr = nullptr;
350 3 : if (git_repository_index(&index_ptr, repo) < 0)
351 0 : return;
352 3 : GitIndex index {index_ptr, git_index_free};
353 3 : git_strarray array = {nullptr, 0};
354 3 : git_index_add_all(index.get(), &array, 0, nullptr, nullptr);
355 3 : git_index_write(index.get());
356 3 : git_strarray_free(&array);
357 3 : }
358 :
359 : void
360 1 : ConversationRepositoryTest::testMerge()
361 : {
362 1 : auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
363 2 : auto repository = ConversationRepository::createConversation(aliceAccount);
364 :
365 : // Assert that repository exists
366 1 : CPPUNIT_ASSERT(repository != nullptr);
367 2 : auto repoPath = fileutils::get_data_dir() / aliceAccount->getAccountID()
368 4 : / "conversations" / repository->id();
369 1 : CPPUNIT_ASSERT(std::filesystem::is_directory(repoPath));
370 :
371 : // Assert that first commit is signed by alice
372 : git_repository* repo;
373 1 : CPPUNIT_ASSERT(git_repository_open(&repo, repoPath.c_str()) == 0);
374 2 : auto id1 = addCommit(repo, aliceAccount, "main", "Commit 1");
375 :
376 1 : git_reference* ref = nullptr;
377 1 : git_commit* commit = nullptr;
378 : git_oid commit_id;
379 1 : git_oid_fromstr(&commit_id, repository->id().c_str());
380 1 : git_commit_lookup(&commit, repo, &commit_id);
381 1 : git_branch_create(&ref, repo, "to_merge", commit, false);
382 1 : git_reference_free(ref);
383 1 : git_repository_set_head(repo, "refs/heads/to_merge");
384 :
385 2 : auto id2 = addCommit(repo, aliceAccount, "to_merge", "Commit 2");
386 1 : git_repository_free(repo);
387 :
388 : // This will create a merge commit
389 1 : repository->merge(id2);
390 :
391 1 : CPPUNIT_ASSERT(repository->log().size() == 4 /* Initial, commit 1, 2, merge */);
392 1 : }
393 :
394 : void
395 1 : ConversationRepositoryTest::testFFMerge()
396 : {
397 1 : auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
398 2 : auto repository = ConversationRepository::createConversation(aliceAccount);
399 :
400 : // Assert that repository exists
401 1 : CPPUNIT_ASSERT(repository != nullptr);
402 2 : auto repoPath = fileutils::get_data_dir() / aliceAccount->getAccountID()
403 4 : / "conversations" / repository->id();
404 1 : CPPUNIT_ASSERT(std::filesystem::is_directory(repoPath));
405 :
406 : // Assert that first commit is signed by alice
407 : git_repository* repo;
408 1 : CPPUNIT_ASSERT(git_repository_open(&repo, repoPath.c_str()) == 0);
409 2 : auto id1 = addCommit(repo, aliceAccount, "main", "Commit 1");
410 :
411 1 : git_reference* ref = nullptr;
412 1 : git_commit* commit = nullptr;
413 : git_oid commit_id;
414 1 : git_oid_fromstr(&commit_id, id1.c_str());
415 1 : git_commit_lookup(&commit, repo, &commit_id);
416 1 : git_branch_create(&ref, repo, "to_merge", commit, false);
417 1 : git_reference_free(ref);
418 1 : git_repository_set_head(repo, "refs/heads/to_merge");
419 :
420 2 : auto id2 = addCommit(repo, aliceAccount, "to_merge", "Commit 2");
421 1 : git_repository_free(repo);
422 :
423 : // This will use a fast forward merge
424 1 : repository->merge(id2);
425 :
426 1 : CPPUNIT_ASSERT(repository->log().size() == 3 /* Initial, commit 1, 2 */);
427 1 : }
428 :
429 : void
430 1 : ConversationRepositoryTest::testDiff()
431 : {
432 1 : auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
433 1 : auto aliceDeviceId = DeviceId(std::string(aliceAccount->currentDeviceId()));
434 1 : auto uri = aliceAccount->getUsername();
435 2 : auto repository = ConversationRepository::createConversation(aliceAccount);
436 :
437 2 : auto id1 = repository->commitMessage("Commit 1");
438 2 : auto id2 = repository->commitMessage("Commit 2");
439 2 : auto id3 = repository->commitMessage("Commit 3");
440 :
441 1 : auto diff = repository->diffStats(id2, id1);
442 1 : CPPUNIT_ASSERT(ConversationRepository::changedFiles(diff).empty());
443 1 : diff = repository->diffStats(id1);
444 1 : auto changedFiles = ConversationRepository::changedFiles(diff);
445 1 : CPPUNIT_ASSERT(!changedFiles.empty());
446 1 : CPPUNIT_ASSERT(changedFiles[0] == "admins/" + uri + ".crt");
447 1 : CPPUNIT_ASSERT(changedFiles[1] == "devices/" + aliceDeviceId.toString() + ".crt");
448 1 : }
449 :
450 : void
451 1 : ConversationRepositoryTest::testMergeProfileWithConflict()
452 : {
453 1 : auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
454 2 : auto repository = ConversationRepository::createConversation(aliceAccount);
455 :
456 : // Assert that repository exists
457 1 : CPPUNIT_ASSERT(repository != nullptr);
458 2 : auto repoPath = fileutils::get_data_dir() / aliceAccount->getAccountID()
459 4 : / "conversations" / repository->id();
460 1 : CPPUNIT_ASSERT(std::filesystem::is_directory(repoPath));
461 :
462 : // Assert that first commit is signed by alice
463 : git_repository* repo;
464 1 : CPPUNIT_ASSERT(git_repository_open(&repo, repoPath.c_str()) == 0);
465 :
466 2 : auto profile = std::ofstream(repoPath / "profile.vcf");
467 1 : if (profile.is_open()) {
468 1 : profile << "TITLE: SWARM\n";
469 1 : profile << "SUBTITLE: Some description\n";
470 1 : profile << "AVATAR: BASE64\n";
471 1 : profile.close();
472 : }
473 1 : addAll(repo);
474 2 : auto id1 = addCommit(repo, aliceAccount, "main", "add profile");
475 1 : profile = std::ofstream(repoPath / "profile.vcf");
476 1 : if (profile.is_open()) {
477 1 : profile << "TITLE: SWARM\n";
478 1 : profile << "SUBTITLE: New description\n";
479 1 : profile << "AVATAR: BASE64\n";
480 1 : profile.close();
481 : }
482 1 : addAll(repo);
483 2 : auto id2 = addCommit(repo, aliceAccount, "main", "modify profile");
484 :
485 1 : git_reference* ref = nullptr;
486 1 : git_commit* commit = nullptr;
487 : git_oid commit_id;
488 1 : git_oid_fromstr(&commit_id, id1.c_str());
489 1 : git_commit_lookup(&commit, repo, &commit_id);
490 1 : git_branch_create(&ref, repo, "to_merge", commit, false);
491 1 : git_reference_free(ref);
492 1 : git_repository_set_head(repo, "refs/heads/to_merge");
493 :
494 1 : profile = std::ofstream(repoPath / "profile.vcf");
495 1 : if (profile.is_open()) {
496 1 : profile << "TITLE: SWARM\n";
497 1 : profile << "SUBTITLE: Another description\n";
498 1 : profile << "AVATAR: BASE64\n";
499 1 : profile.close();
500 : }
501 1 : addAll(repo);
502 2 : auto id3 = addCommit(repo, aliceAccount, "to_merge", "modify profile merge");
503 :
504 : // This will create a merge commit
505 1 : repository->merge(id3);
506 1 : CPPUNIT_ASSERT(repository->log().size() == 5 /* Initial, add, modify 1, modify 2, merge */);
507 1 : }
508 :
509 : } // namespace test
510 : } // namespace jami
511 :
512 1 : RING_TEST_RUNNER(jami::test::ConversationRepositoryTest::name())
|