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