Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/keepassxreboot/keepassxc.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'tests/TestMerge.cpp')
-rw-r--r--tests/TestMerge.cpp597
1 files changed, 597 insertions, 0 deletions
diff --git a/tests/TestMerge.cpp b/tests/TestMerge.cpp
new file mode 100644
index 000000000..8f38a2f85
--- /dev/null
+++ b/tests/TestMerge.cpp
@@ -0,0 +1,597 @@
+/*
+ * Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "TestMerge.h"
+#include "TestGlobal.h"
+
+#include "core/Metadata.h"
+#include "crypto/Crypto.h"
+
+QTEST_GUILESS_MAIN(TestMerge)
+
+void TestMerge::initTestCase()
+{
+ qRegisterMetaType<Entry*>("Entry*");
+ qRegisterMetaType<Group*>("Group*");
+ QVERIFY(Crypto::init());
+}
+
+/**
+ * Merge an existing database into a new one.
+ * All the entries of the existing should end
+ * up in the new one.
+ */
+void TestMerge::testMergeIntoNew()
+{
+ Database* dbSource = createTestDatabase();
+ Database* dbDestination = new Database();
+
+ dbDestination->merge(dbSource);
+
+ QCOMPARE(dbDestination->rootGroup()->children().size(), 2);
+ QCOMPARE(dbDestination->rootGroup()->children().at(0)->entries().size(), 2);
+ // Test for retention of history
+ QCOMPARE(dbDestination->rootGroup()->children().at(0)->entries().at(0)->historyItems().isEmpty(), false);
+
+ delete dbDestination;
+ delete dbSource;
+}
+
+/**
+ * Merging when no changes occured should not
+ * have any side effect.
+ */
+void TestMerge::testMergeNoChanges()
+{
+ Database* dbDestination = createTestDatabase();
+
+ Database* dbSource = new Database();
+ dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries));
+
+ QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
+ QCOMPARE(dbSource->rootGroup()->entriesRecursive().size(), 2);
+
+ dbDestination->merge(dbSource);
+
+ QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
+ QCOMPARE(dbSource->rootGroup()->entriesRecursive().size(), 2);
+
+ dbDestination->merge(dbSource);
+
+ QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
+ QCOMPARE(dbSource->rootGroup()->entriesRecursive().size(), 2);
+
+ delete dbDestination;
+ delete dbSource;
+}
+
+/**
+ * If the entry is updated in the source database, the update
+ * should propagate in the destination database.
+ */
+void TestMerge::testResolveConflictNewer()
+{
+ Database* dbDestination = createTestDatabase();
+
+ Database* dbSource = new Database();
+ dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries));
+
+ // sanity check
+ Group* group1 = dbSource->rootGroup()->findChildByName("group1");
+ QVERIFY(group1 != nullptr);
+ QCOMPARE(group1->entries().size(), 2);
+
+ Entry* entry1 = dbSource->rootGroup()->findEntry("entry1");
+ QVERIFY(entry1 != nullptr);
+
+ // Make sure the two changes have a different timestamp.
+ QTest::qSleep(1);
+ // make this entry newer than in destination db
+ entry1->beginUpdate();
+ entry1->setPassword("password");
+ entry1->endUpdate();
+
+ dbDestination->merge(dbSource);
+
+ // sanity check
+ group1 = dbDestination->rootGroup()->findChildByName("group1");
+ QVERIFY(group1 != nullptr);
+ QCOMPARE(group1->entries().size(), 2);
+
+ entry1 = dbDestination->rootGroup()->findEntry("entry1");
+ QVERIFY(entry1 != nullptr);
+ QVERIFY(entry1->group() != nullptr);
+ QCOMPARE(entry1->password(), QString("password"));
+
+ // When updating an entry, it should not end up in the
+ // deleted objects.
+ for (DeletedObject deletedObject : dbDestination->deletedObjects()) {
+ QVERIFY(deletedObject.uuid != entry1->uuid());
+ }
+
+ delete dbDestination;
+ delete dbSource;
+}
+
+/**
+ * If the entry is updated in the source database, and the
+ * destination database after, the entry should remain the
+ * same.
+ */
+void TestMerge::testResolveConflictOlder()
+{
+ Database* dbDestination = createTestDatabase();
+
+ Database* dbSource = new Database();
+ dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries));
+
+ // sanity check
+ Group* group1 = dbSource->rootGroup()->findChildByName("group1");
+ QVERIFY(group1 != nullptr);
+ QCOMPARE(group1->entries().size(), 2);
+
+ Entry* entry1 = dbSource->rootGroup()->findEntry("entry1");
+ QVERIFY(entry1 != nullptr);
+
+ // Make sure the two changes have a different timestamp.
+ QTest::qSleep(1);
+ // make this entry newer than in destination db
+ entry1->beginUpdate();
+ entry1->setPassword("password1");
+ entry1->endUpdate();
+
+ entry1 = dbDestination->rootGroup()->findEntry("entry1");
+ QVERIFY(entry1 != nullptr);
+
+ // Make sure the two changes have a different timestamp.
+ QTest::qSleep(1);
+ // make this entry newer than in destination db
+ entry1->beginUpdate();
+ entry1->setPassword("password2");
+ entry1->endUpdate();
+
+ dbDestination->merge(dbSource);
+
+ // sanity check
+ group1 = dbDestination->rootGroup()->findChildByName("group1");
+ QVERIFY(group1 != nullptr);
+ QCOMPARE(group1->entries().size(), 2);
+
+ entry1 = dbDestination->rootGroup()->findEntry("entry1");
+ QVERIFY(entry1 != nullptr);
+ QCOMPARE(entry1->password(), QString("password2"));
+
+ // When updating an entry, it should not end up in the
+ // deleted objects.
+ for (DeletedObject deletedObject : dbDestination->deletedObjects()) {
+ QVERIFY(deletedObject.uuid != entry1->uuid());
+ }
+
+ delete dbDestination;
+ delete dbSource;
+}
+
+/**
+ * Tests the KeepBoth merge mode.
+ */
+void TestMerge::testResolveConflictKeepBoth()
+{
+ Database* dbDestination = createTestDatabase();
+
+ Database* dbSource = new Database();
+ dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneIncludeHistory, Group::CloneIncludeEntries));
+
+ // sanity check
+ QCOMPARE(dbDestination->rootGroup()->children().at(0)->entries().size(), 2);
+
+ // make this entry newer than in original db
+ Entry* updatedEntry = dbDestination->rootGroup()->children().at(0)->entries().at(0);
+ TimeInfo updatedTimeInfo = updatedEntry->timeInfo();
+ updatedTimeInfo.setLastModificationTime(updatedTimeInfo.lastModificationTime().addYears(1));
+ updatedEntry->setTimeInfo(updatedTimeInfo);
+
+ dbDestination->rootGroup()->setMergeMode(Group::MergeMode::KeepBoth);
+
+ dbDestination->merge(dbSource);
+
+ // one entry is duplicated because of mode
+ QCOMPARE(dbDestination->rootGroup()->children().at(0)->entries().size(), 3);
+ QCOMPARE(dbDestination->rootGroup()->children().at(0)->entries().at(0)->historyItems().isEmpty(), false);
+ // the older entry was merged from the other db as last in the group
+ Entry* olderEntry = dbDestination->rootGroup()->children().at(0)->entries().at(2);
+ QVERIFY2(olderEntry->attributes()->hasKey("merged"), "older entry is marked with an attribute \"merged\"");
+ QCOMPARE(olderEntry->historyItems().isEmpty(), false);
+
+ QVERIFY2(olderEntry->uuid().toHex() != updatedEntry->uuid().toHex(),
+ "KeepBoth should not reuse the UUIDs when cloning.");
+
+ delete dbSource;
+ delete dbDestination;
+}
+
+/**
+ * The location of an entry should be updated in the
+ * destination database.
+ */
+void TestMerge::testMoveEntry()
+{
+ Database* dbDestination = createTestDatabase();
+
+ Database* dbSource = new Database();
+ dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries));
+
+ Entry* entry1 = dbSource->rootGroup()->findEntry("entry1");
+ QVERIFY(entry1 != nullptr);
+
+ Group* group2 = dbSource->rootGroup()->findChildByName("group2");
+ QVERIFY(group2 != nullptr);
+
+ // Make sure the two changes have a different timestamp.
+ QTest::qSleep(1);
+ entry1->setGroup(group2);
+ QCOMPARE(entry1->group()->name(), QString("group2"));
+
+ dbDestination->merge(dbSource);
+
+ entry1 = dbDestination->rootGroup()->findEntry("entry1");
+ QVERIFY(entry1 != nullptr);
+ QCOMPARE(entry1->group()->name(), QString("group2"));
+ QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
+
+ delete dbDestination;
+ delete dbSource;
+}
+
+/**
+ * The location of an entry should be updated in the
+ * destination database, but changes from the destination
+ * database should be preserved.
+ */
+void TestMerge::testMoveEntryPreserveChanges()
+{
+ Database* dbDestination = createTestDatabase();
+
+ Database* dbSource = new Database();
+ dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries));
+
+ Entry* entry1 = dbSource->rootGroup()->findEntry("entry1");
+ QVERIFY(entry1 != nullptr);
+
+ Group* group2 = dbSource->rootGroup()->findChildByName("group2");
+ QVERIFY(group2 != nullptr);
+
+ QTest::qSleep(1);
+ entry1->setGroup(group2);
+ QCOMPARE(entry1->group()->name(), QString("group2"));
+
+ entry1 = dbDestination->rootGroup()->findEntry("entry1");
+ QVERIFY(entry1 != nullptr);
+
+ QTest::qSleep(1);
+ entry1->beginUpdate();
+ entry1->setPassword("password");
+ entry1->endUpdate();
+
+ dbDestination->merge(dbSource);
+
+ entry1 = dbDestination->rootGroup()->findEntry("entry1");
+ QVERIFY(entry1 != nullptr);
+ QCOMPARE(entry1->group()->name(), QString("group2"));
+ QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
+ QCOMPARE(entry1->password(), QString("password"));
+
+ delete dbDestination;
+ delete dbSource;
+}
+
+void TestMerge::testCreateNewGroups()
+{
+ Database* dbDestination = createTestDatabase();
+
+ Database* dbSource = new Database();
+ dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries));
+
+ QTest::qSleep(1);
+ Group* group3 = new Group();
+ group3->setName("group3");
+ group3->setUuid(Uuid::random());
+ group3->setParent(dbSource->rootGroup());
+
+ dbDestination->merge(dbSource);
+
+ group3 = dbDestination->rootGroup()->findChildByName("group3");
+ QVERIFY(group3 != nullptr);
+ QCOMPARE(group3->name(), QString("group3"));
+
+ delete dbDestination;
+ delete dbSource;
+}
+
+void TestMerge::testMoveEntryIntoNewGroup()
+{
+ Database* dbDestination = createTestDatabase();
+
+ Database* dbSource = new Database();
+ dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries));
+
+ QTest::qSleep(1);
+ Group* group3 = new Group();
+ group3->setName("group3");
+ group3->setUuid(Uuid::random());
+ group3->setParent(dbSource->rootGroup());
+
+ Entry* entry1 = dbSource->rootGroup()->findEntry("entry1");
+ entry1->setGroup(group3);
+
+ dbDestination->merge(dbSource);
+
+ QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
+
+ group3 = dbDestination->rootGroup()->findChildByName("group3");
+ QVERIFY(group3 != nullptr);
+ QCOMPARE(group3->name(), QString("group3"));
+ QCOMPARE(group3->entries().size(), 1);
+
+ entry1 = dbDestination->rootGroup()->findEntry("entry1");
+ QVERIFY(entry1 != nullptr);
+ QCOMPARE(entry1->group()->name(), QString("group3"));
+
+ delete dbDestination;
+ delete dbSource;
+}
+
+/**
+ * Even though the entries' locations are no longer
+ * the same, we will keep associating them.
+ */
+void TestMerge::testUpdateEntryDifferentLocation()
+{
+ Database* dbDestination = createTestDatabase();
+
+ Database* dbSource = new Database();
+ dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries));
+
+ Group* group3 = new Group();
+ group3->setName("group3");
+ group3->setUuid(Uuid::random());
+ group3->setParent(dbDestination->rootGroup());
+
+ Entry* entry1 = dbDestination->rootGroup()->findEntry("entry1");
+ QVERIFY(entry1 != nullptr);
+ entry1->setGroup(group3);
+ Uuid uuidBeforeSyncing = entry1->uuid();
+
+ // Change the entry in the source db.
+ QTest::qSleep(1);
+ entry1 = dbSource->rootGroup()->findEntry("entry1");
+ QVERIFY(entry1 != nullptr);
+ entry1->beginUpdate();
+ entry1->setUsername("username");
+ entry1->endUpdate();
+
+ dbDestination->merge(dbSource);
+
+ QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
+
+ entry1 = dbDestination->rootGroup()->findEntry("entry1");
+ QVERIFY(entry1 != nullptr);
+ QVERIFY(entry1->group() != nullptr);
+ QCOMPARE(entry1->username(), QString("username"));
+ QCOMPARE(entry1->group()->name(), QString("group3"));
+ QCOMPARE(uuidBeforeSyncing, entry1->uuid());
+
+ delete dbDestination;
+ delete dbSource;
+}
+
+/**
+ * Groups should be updated using the uuids.
+ */
+void TestMerge::testUpdateGroup()
+{
+ Database* dbDestination = createTestDatabase();
+
+ Database* dbSource = new Database();
+ dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries));
+
+ QTest::qSleep(1);
+
+ Group* group2 = dbSource->rootGroup()->findChildByName("group2");
+ group2->setName("group2 renamed");
+ group2->setNotes("updated notes");
+ Uuid customIconId = Uuid::random();
+ QImage customIcon;
+ dbSource->metadata()->addCustomIcon(customIconId, customIcon);
+ group2->setIcon(customIconId);
+
+ Entry* entry1 = dbSource->rootGroup()->findEntry("entry1");
+ QVERIFY(entry1 != nullptr);
+ entry1->setGroup(group2);
+ entry1->setTitle("entry1 renamed");
+ Uuid uuidBeforeSyncing = entry1->uuid();
+
+ dbDestination->merge(dbSource);
+
+ QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
+
+ entry1 = dbDestination->rootGroup()->findEntry("entry1 renamed");
+ QVERIFY(entry1 != nullptr);
+ QVERIFY(entry1->group() != nullptr);
+ QCOMPARE(entry1->group()->name(), QString("group2 renamed"));
+ QCOMPARE(uuidBeforeSyncing, entry1->uuid());
+
+ group2 = dbDestination->rootGroup()->findChildByName("group2 renamed");
+ QCOMPARE(group2->notes(), QString("updated notes"));
+ QCOMPARE(group2->iconUuid(), customIconId);
+
+ delete dbDestination;
+ delete dbSource;
+}
+
+void TestMerge::testUpdateGroupLocation()
+{
+ Database* dbDestination = createTestDatabase();
+ Group* group3 = new Group();
+ Uuid group3Uuid = Uuid::random();
+ group3->setUuid(group3Uuid);
+ group3->setName("group3");
+ group3->setParent(dbDestination->rootGroup()->findChildByName("group1"));
+
+ Database* dbSource = new Database();
+ dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries));
+
+ // Sanity check
+ group3 = dbSource->rootGroup()->findChildByUuid(group3Uuid);
+ QVERIFY(group3 != nullptr);
+
+ QTest::qSleep(1);
+
+ group3->setParent(dbSource->rootGroup()->findChildByName("group2"));
+
+ dbDestination->merge(dbSource);
+ group3 = dbDestination->rootGroup()->findChildByUuid(group3Uuid);
+ QVERIFY(group3 != nullptr);
+ QCOMPARE(group3->parent(), dbDestination->rootGroup()->findChildByName("group2"));
+
+ dbDestination->merge(dbSource);
+ group3 = dbDestination->rootGroup()->findChildByUuid(group3Uuid);
+ QVERIFY(group3 != nullptr);
+ QCOMPARE(group3->parent(), dbDestination->rootGroup()->findChildByName("group2"));
+
+ delete dbDestination;
+ delete dbSource;
+}
+
+/**
+ * The first merge should create new entries, the
+ * second should only sync them, since they have
+ * been created with the same UUIDs.
+ */
+void TestMerge::testMergeAndSync()
+{
+ Database* dbDestination = new Database();
+ Database* dbSource = createTestDatabase();
+
+ QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 0);
+
+ dbDestination->merge(dbSource);
+
+ QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
+
+ dbDestination->merge(dbSource);
+
+ // Still only 2 entries, since now we detect which are already present.
+ QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
+
+ delete dbDestination;
+ delete dbSource;
+}
+
+/**
+ * Custom icons should be brought over when merging.
+ */
+void TestMerge::testMergeCustomIcons()
+{
+ Database* dbDestination = new Database();
+ Database* dbSource = createTestDatabase();
+
+ Uuid customIconId = Uuid::random();
+ QImage customIcon;
+
+ dbSource->metadata()->addCustomIcon(customIconId, customIcon);
+ // Sanity check.
+ QVERIFY(dbSource->metadata()->containsCustomIcon(customIconId));
+
+ dbDestination->merge(dbSource);
+
+ QVERIFY(dbDestination->metadata()->containsCustomIcon(customIconId));
+
+ delete dbDestination;
+ delete dbSource;
+}
+
+/**
+ * If the group is updated in the source database, and the
+ * destination database after, the group should remain the
+ * same.
+ */
+void TestMerge::testResolveGroupConflictOlder()
+{
+ Database* dbDestination = createTestDatabase();
+
+ Database* dbSource = new Database();
+ dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries));
+
+ // sanity check
+ Group* group1 = dbSource->rootGroup()->findChildByName("group1");
+ QVERIFY(group1 != nullptr);
+
+ // Make sure the two changes have a different timestamp.
+ QTest::qSleep(1);
+ group1->setName("group1 updated in source");
+
+ // Make sure the two changes have a different timestamp.
+ QTest::qSleep(1);
+
+ group1 = dbDestination->rootGroup()->findChildByName("group1");
+ group1->setName("group1 updated in destination");
+
+ dbDestination->merge(dbSource);
+
+ // sanity check
+ group1 = dbDestination->rootGroup()->findChildByName("group1 updated in destination");
+ QVERIFY(group1 != nullptr);
+
+ delete dbDestination;
+ delete dbSource;
+}
+
+
+Database* TestMerge::createTestDatabase()
+{
+ Database* db = new Database();
+
+ Group* group1 = new Group();
+ group1->setName("group1");
+ group1->setUuid(Uuid::random());
+
+ Group* group2 = new Group();
+ group2->setName("group2");
+ group2->setUuid(Uuid::random());
+
+ Entry* entry1 = new Entry();
+ Entry* entry2 = new Entry();
+
+ // Give Entry 1 a history
+ entry1->beginUpdate();
+ entry1->setGroup(group1);
+ entry1->setUuid(Uuid::random());
+ entry1->setTitle("entry1");
+ entry1->endUpdate();
+
+ // Give Entry 2 a history
+ entry2->beginUpdate();
+ entry2->setGroup(group1);
+ entry2->setUuid(Uuid::random());
+ entry2->setTitle("entry2");
+ entry2->endUpdate();
+
+ group1->setParent(db->rootGroup());
+ group2->setParent(db->rootGroup());
+
+ return db;
+}