/* * Copyright (C) 2017 KeePassXC Team * * 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 . */ #include "TestMerge.h" #include "TestGlobal.h" #include "core/Metadata.h" #include "crypto/Crypto.h" QTEST_GUILESS_MAIN(TestMerge) void TestMerge::initTestCase() { qRegisterMetaType("Entry*"); qRegisterMetaType("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; }