diff --git a/.gitignore b/.gitignore index 0521a42e3d..2d9bb4af7a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ release*/ *.kdev4 \.vscode/ +*.swp diff --git a/src/core/Group.cpp b/src/core/Group.cpp index e199ae43fb..92e0269e51 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -488,18 +488,17 @@ Entry* Group::findEntry(QString entryId) { Q_ASSERT(!entryId.isNull()); + Entry* entry; if (Uuid::isUuid(entryId)) { - Uuid entryUuid = Uuid::fromHex(entryId); - for (Entry* entry : entriesRecursive(false)) { - if (entry->uuid() == entryUuid) { - return entry; - } + entry = findEntryByUuid(Uuid::fromHex(entryId)); + if (entry) { + return entry; } } - Entry* entry = findEntryByPath(entryId); + entry = findEntryByPath(entryId); if (entry) { - return entry; + return entry; } for (Entry* entry : entriesRecursive(false)) { @@ -514,7 +513,7 @@ Entry* Group::findEntry(QString entryId) Entry* Group::findEntryByUuid(const Uuid& uuid) { Q_ASSERT(!uuid.isNull()); - for (Entry* entry : asConst(m_entries)) { + for (Entry* entry : entriesRecursive(false)) { if (entry->uuid() == uuid) { return entry; } @@ -575,7 +574,6 @@ Group* Group::findGroupByPath(QString groupPath, QString basePath) } return nullptr; - } QString Group::print(bool recursive, int depth) @@ -655,25 +653,44 @@ QSet Group::customIconsRecursive() const void Group::merge(const Group* other) { + + Group* rootGroup = this; + while (rootGroup->parentGroup()) { + rootGroup = rootGroup->parentGroup(); + } + // merge entries const QList dbEntries = other->entries(); for (Entry* entry : dbEntries) { - // entries are searched by uuid - if (!findEntryByUuid(entry->uuid())) { + + Entry* existingEntry = rootGroup->findEntryByUuid(entry->uuid()); + + // This entry does not exist at all. Create it. + if (!existingEntry) { + qDebug("New entry %s detected. Creating it.", qPrintable(entry->title())); entry->clone(Entry::CloneNoFlags)->setGroup(this); + // Entry is already present in the database. Update it. } else { - resolveConflict(findEntryByUuid(entry->uuid()), entry); + bool locationChanged = existingEntry->timeInfo().locationChanged() < entry->timeInfo().locationChanged(); + if (locationChanged && existingEntry->group() != this) { + existingEntry->setGroup(this); + qDebug("Location changed for entry %s. Updating it", qPrintable(existingEntry->title())); + } + resolveConflict(existingEntry, entry); } } - // merge groups (recursively) + // merge groups recursively const QList dbChildren = other->children(); for (Group* group : dbChildren) { // groups are searched by name instead of uuid if (findChildByName(group->name())) { findChildByName(group->name())->merge(group); } else { - group->setParent(this); + qDebug("New group %s detected. Creating it.", qPrintable(group->name())); + Group* newGroup = group->clone(Entry::CloneNoFlags, true); + newGroup->setParent(this); + newGroup->merge(group); } } @@ -691,7 +708,7 @@ Group* Group::findChildByName(const QString& name) return nullptr; } -Group* Group::clone(Entry::CloneFlags entryFlags) const +Group* Group::clone(Entry::CloneFlags entryFlags, bool shallow) const { Group* clonedGroup = new Group(); @@ -700,16 +717,18 @@ Group* Group::clone(Entry::CloneFlags entryFlags) const clonedGroup->setUuid(Uuid::random()); clonedGroup->m_data = m_data; - const QList entryList = entries(); - for (Entry* entry : entryList) { - Entry* clonedEntry = entry->clone(entryFlags); - clonedEntry->setGroup(clonedGroup); - } + if (!shallow) { + const QList entryList = entries(); + for (Entry* entry : entryList) { + Entry* clonedEntry = entry->clone(entryFlags); + clonedEntry->setGroup(clonedGroup); + } - const QList childrenGroups = children(); - for (Group* groupChild : childrenGroups) { - Group* clonedGroupChild = groupChild->clone(entryFlags); - clonedGroupChild->setParent(clonedGroup); + const QList childrenGroups = children(); + for (Group* groupChild : childrenGroups) { + Group* clonedGroupChild = groupChild->clone(entryFlags); + clonedGroupChild->setParent(clonedGroup); + } } clonedGroup->setUpdateTimeinfo(true); @@ -829,8 +848,7 @@ void Group::markOlderEntry(Entry* entry) { entry->attributes()->set( "merged", - QString("older entry merged from database \"%1\"").arg(entry->group()->database()->metadata()->name()) - ); + QString("older entry merged from database \"%1\"").arg(entry->group()->database()->metadata()->name())); } bool Group::resolveSearchingEnabled() const @@ -880,32 +898,34 @@ void Group::resolveConflict(Entry* existingEntry, Entry* otherEntry) Entry* clonedEntry; - switch(mergeMode()) { - case KeepBoth: - // if one entry is newer, create a clone and add it to the group - if (timeExisting > timeOther) { - clonedEntry = otherEntry->clone(Entry::CloneNoFlags); - clonedEntry->setGroup(this); - markOlderEntry(clonedEntry); - } else if (timeExisting < timeOther) { - clonedEntry = otherEntry->clone(Entry::CloneNoFlags); - clonedEntry->setGroup(this); - markOlderEntry(existingEntry); - } - break; - case KeepNewer: - if (timeExisting < timeOther) { - // only if other entry is newer, replace existing one - removeEntry(existingEntry); - addEntry(otherEntry->clone(Entry::CloneNoFlags)); - } - - break; - case KeepExisting: - break; - default: - // do nothing - break; + switch (mergeMode()) { + case KeepBoth: + // if one entry is newer, create a clone and add it to the group + if (timeExisting > timeOther) { + clonedEntry = otherEntry->clone(Entry::CloneNewUuid); + clonedEntry->setGroup(this); + markOlderEntry(clonedEntry); + } else if (timeExisting < timeOther) { + clonedEntry = otherEntry->clone(Entry::CloneNewUuid); + clonedEntry->setGroup(this); + markOlderEntry(existingEntry); + } + break; + case KeepNewer: + if (timeExisting < timeOther) { + qDebug("Updating entry %s.", qPrintable(existingEntry->title())); + // only if other entry is newer, replace existing one + Group* currentGroup = existingEntry->group(); + currentGroup->removeEntry(existingEntry); + otherEntry->clone(Entry::CloneNoFlags)->setGroup(currentGroup); + } + + break; + case KeepExisting: + break; + default: + // do nothing + break; } } @@ -928,5 +948,4 @@ QStringList Group::locate(QString locateTerm, QString currentPath) } return response; - } diff --git a/src/core/Group.h b/src/core/Group.h index 4e76538e01..73b791590a 100644 --- a/src/core/Group.h +++ b/src/core/Group.h @@ -117,13 +117,14 @@ class Group : public QObject QList groupsRecursive(bool includeSelf); QSet customIconsRecursive() const; /** - * Creates a duplicate of this group including all child entries and groups. + * Creates a duplicate of this group including all child entries and groups (if not shallow). * The exceptions are that the returned group doesn't have a parent group * and all TimeInfo attributes are set to the current time. * Note that you need to copy the custom icons manually when inserting the * new group into another database. */ - Group* clone(Entry::CloneFlags entryFlags = Entry::CloneNewUuid | Entry::CloneResetTimeInfo) const; + Group* clone(Entry::CloneFlags entryFlags = Entry::CloneNewUuid | Entry::CloneResetTimeInfo, + bool shallow = false) const; void copyDataFrom(const Group* other); void merge(const Group* other); QString print(bool recursive = false, int depth = 0); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2a420270ab..33baf564ff 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -158,6 +158,9 @@ endif() add_unit_test(NAME testentry SOURCES TestEntry.cpp LIBS ${TEST_LIBRARIES}) +add_unit_test(NAME testmerge SOURCES TestMerge.cpp + LIBS ${TEST_LIBRARIES}) + add_unit_test(NAME testtotp SOURCES TestTotp.cpp LIBS ${TEST_LIBRARIES}) diff --git a/tests/TestGroup.cpp b/tests/TestGroup.cpp index 5553feae85..efdcb32e69 100644 --- a/tests/TestGroup.cpp +++ b/tests/TestGroup.cpp @@ -452,123 +452,6 @@ void TestGroup::testCopyCustomIcons() delete dbSource; } -void TestGroup::testMerge() -{ - Group* group1 = new Group(); - group1->setName("group 1"); - Group* group2 = new Group(); - group2->setName("group 2"); - - Entry* entry1 = new Entry(); - Entry* entry2 = new Entry(); - - entry1->setGroup(group1); - entry1->setUuid(Uuid::random()); - entry2->setGroup(group1); - entry2->setUuid(Uuid::random()); - - group2->merge(group1); - - QCOMPARE(group1->entries().size(), 2); - QCOMPARE(group2->entries().size(), 2); -} - -void TestGroup::testMergeDatabase() -{ - Database* dbSource = createMergeTestDatabase(); - Database* dbDest = new Database(); - - dbDest->merge(dbSource); - - QCOMPARE(dbDest->rootGroup()->children().size(), 2); - QCOMPARE(dbDest->rootGroup()->children().at(0)->entries().size(), 2); - - delete dbDest; - delete dbSource; -} - -void TestGroup::testMergeConflict() -{ - Database* dbSource = createMergeTestDatabase(); - - // test merging updated entries - // falls back to KeepBoth mode - Database* dbCopy = new Database(); - dbCopy->setRootGroup(dbSource->rootGroup()->clone(Entry::CloneNoFlags)); - - // sanity check - QCOMPARE(dbCopy->rootGroup()->children().at(0)->entries().size(), 2); - - // make this entry newer than in original db - Entry* updatedEntry = dbCopy->rootGroup()->children().at(0)->entries().at(0); - TimeInfo updatedTimeInfo = updatedEntry->timeInfo(); - updatedTimeInfo.setLastModificationTime(updatedTimeInfo.lastModificationTime().addYears(1)); - updatedEntry->setTimeInfo(updatedTimeInfo); - - dbCopy->merge(dbSource); - - // one entry is duplicated because of mode - QCOMPARE(dbCopy->rootGroup()->children().at(0)->entries().size(), 2); - - delete dbSource; - delete dbCopy; -} - -void TestGroup::testMergeConflictKeepBoth() -{ - Database* dbSource = createMergeTestDatabase(); - - // test merging updated entries - // falls back to KeepBoth mode - Database* dbCopy = new Database(); - dbCopy->setRootGroup(dbSource->rootGroup()->clone(Entry::CloneNoFlags)); - - // sanity check - QCOMPARE(dbCopy->rootGroup()->children().at(0)->entries().size(), 2); - - // make this entry newer than in original db - Entry* updatedEntry = dbCopy->rootGroup()->children().at(0)->entries().at(0); - TimeInfo updatedTimeInfo = updatedEntry->timeInfo(); - updatedTimeInfo.setLastModificationTime(updatedTimeInfo.lastModificationTime().addYears(1)); - updatedEntry->setTimeInfo(updatedTimeInfo); - - dbCopy->rootGroup()->setMergeMode(Group::MergeMode::KeepBoth); - - dbCopy->merge(dbSource); - - // one entry is duplicated because of mode - QCOMPARE(dbCopy->rootGroup()->children().at(0)->entries().size(), 3); - // the older entry was merged from the other db as last in the group - Entry* olderEntry = dbCopy->rootGroup()->children().at(0)->entries().at(2); - QVERIFY2(olderEntry->attributes()->hasKey("merged"), "older entry is marked with an attribute \"merged\""); - - delete dbSource; - delete dbCopy; -} - -Database* TestGroup::createMergeTestDatabase() -{ - Database* db = new Database(); - - Group* group1 = new Group(); - group1->setName("group 1"); - Group* group2 = new Group(); - group2->setName("group 2"); - - Entry* entry1 = new Entry(); - Entry* entry2 = new Entry(); - - entry1->setGroup(group1); - entry1->setUuid(Uuid::random()); - entry2->setGroup(group1); - entry2->setUuid(Uuid::random()); - - group1->setParent(db->rootGroup()); - group2->setParent(db->rootGroup()); - - return db; -} - void TestGroup::testFindEntry() { Database* db = new Database(); diff --git a/tests/TestGroup.h b/tests/TestGroup.h index 16f4906ee6..fabe860af8 100644 --- a/tests/TestGroup.h +++ b/tests/TestGroup.h @@ -35,17 +35,10 @@ private slots: void testCopyCustomIcon(); void testClone(); void testCopyCustomIcons(); - void testMerge(); - void testMergeConflict(); - void testMergeDatabase(); - void testMergeConflictKeepBoth(); void testFindEntry(); void testFindGroupByPath(); void testPrint(); void testLocate(); - -private: - Database* createMergeTestDatabase(); }; #endif // KEEPASSX_TESTGROUP_H diff --git a/tests/TestMerge.cpp b/tests/TestMerge.cpp new file mode 100644 index 0000000000..e9876f1839 --- /dev/null +++ b/tests/TestMerge.cpp @@ -0,0 +1,445 @@ +/* + * 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 +#include + +#include "core/Database.h" +#include "core/Group.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); + + 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)); + + 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)); + + // 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); + 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)); + + // 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::CloneNoFlags)); + + // 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); + // 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\""); + + 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)); + + 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)); + + 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)); + + QTest::qSleep(1); + Group* group3 = new Group(); + group3->setName("group3"); + 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)); + + QTest::qSleep(1); + Group* group3 = new Group(); + group3->setName("group3"); + 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* group3 = new Group(); + group3->setName("group3"); + 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; +} + +/** + * 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; +} + +Database* TestMerge::createTestDatabase() +{ + Database* db = new Database(); + + Group* group1 = new Group(); + group1->setName("group1"); + Group* group2 = new Group(); + group2->setName("group2"); + + Entry* entry1 = new Entry(); + Entry* entry2 = new Entry(); + + entry1->setGroup(group1); + entry1->setUuid(Uuid::random()); + entry1->setTitle("entry1"); + entry2->setGroup(group1); + entry2->setUuid(Uuid::random()); + entry2->setTitle("entry2"); + + group1->setParent(db->rootGroup()); + group2->setParent(db->rootGroup()); + + return db; +} diff --git a/tests/TestMerge.h b/tests/TestMerge.h new file mode 100644 index 0000000000..bfb18c3059 --- /dev/null +++ b/tests/TestMerge.h @@ -0,0 +1,46 @@ +/* + * 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 . + */ + +#ifndef KEEPASSX_TESTMERGE_H +#define KEEPASSX_TESTMERGE_H + +#include "core/Database.h" +#include + +class TestMerge : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + void testMergeIntoNew(); + void testMergeNoChanges(); + void testResolveConflictNewer(); + void testResolveConflictOlder(); + void testResolveConflictKeepBoth(); + void testMoveEntry(); + void testMoveEntryPreserveChanges(); + void testMoveEntryIntoNewGroup(); + void testCreateNewGroups(); + void testUpdateEntryDifferentLocation(); + void testMergeAndSync(); + +private: + Database* createTestDatabase(); +}; + +#endif // KEEPASSX_TESTMERGE_H