Feature : Update entries across groups when merging (#807)

* Feature : Update entries across groups when merging

* Styling
This commit is contained in:
louib 2017-09-05 10:28:47 -04:00 committed by GitHub
parent 321d4e9c87
commit 1220b7d501
8 changed files with 569 additions and 178 deletions

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ release*/
*.kdev4 *.kdev4
\.vscode/ \.vscode/
*.swp

View File

@ -488,18 +488,17 @@ Entry* Group::findEntry(QString entryId)
{ {
Q_ASSERT(!entryId.isNull()); Q_ASSERT(!entryId.isNull());
Entry* entry;
if (Uuid::isUuid(entryId)) { if (Uuid::isUuid(entryId)) {
Uuid entryUuid = Uuid::fromHex(entryId); entry = findEntryByUuid(Uuid::fromHex(entryId));
for (Entry* entry : entriesRecursive(false)) { if (entry) {
if (entry->uuid() == entryUuid) { return entry;
return entry;
}
} }
} }
Entry* entry = findEntryByPath(entryId); entry = findEntryByPath(entryId);
if (entry) { if (entry) {
return entry; return entry;
} }
for (Entry* entry : entriesRecursive(false)) { for (Entry* entry : entriesRecursive(false)) {
@ -514,7 +513,7 @@ Entry* Group::findEntry(QString entryId)
Entry* Group::findEntryByUuid(const Uuid& uuid) Entry* Group::findEntryByUuid(const Uuid& uuid)
{ {
Q_ASSERT(!uuid.isNull()); Q_ASSERT(!uuid.isNull());
for (Entry* entry : asConst(m_entries)) { for (Entry* entry : entriesRecursive(false)) {
if (entry->uuid() == uuid) { if (entry->uuid() == uuid) {
return entry; return entry;
} }
@ -575,7 +574,6 @@ Group* Group::findGroupByPath(QString groupPath, QString basePath)
} }
return nullptr; return nullptr;
} }
QString Group::print(bool recursive, int depth) QString Group::print(bool recursive, int depth)
@ -655,25 +653,44 @@ QSet<Uuid> Group::customIconsRecursive() const
void Group::merge(const Group* other) void Group::merge(const Group* other)
{ {
Group* rootGroup = this;
while (rootGroup->parentGroup()) {
rootGroup = rootGroup->parentGroup();
}
// merge entries // merge entries
const QList<Entry*> dbEntries = other->entries(); const QList<Entry*> dbEntries = other->entries();
for (Entry* entry : dbEntries) { 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->clone(Entry::CloneNoFlags)->setGroup(this);
// Entry is already present in the database. Update it.
} else { } 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<Group*> dbChildren = other->children(); const QList<Group*> dbChildren = other->children();
for (Group* group : dbChildren) { for (Group* group : dbChildren) {
// groups are searched by name instead of uuid // groups are searched by name instead of uuid
if (findChildByName(group->name())) { if (findChildByName(group->name())) {
findChildByName(group->name())->merge(group); findChildByName(group->name())->merge(group);
} else { } 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; return nullptr;
} }
Group* Group::clone(Entry::CloneFlags entryFlags) const Group* Group::clone(Entry::CloneFlags entryFlags, bool shallow) const
{ {
Group* clonedGroup = new Group(); Group* clonedGroup = new Group();
@ -700,16 +717,18 @@ Group* Group::clone(Entry::CloneFlags entryFlags) const
clonedGroup->setUuid(Uuid::random()); clonedGroup->setUuid(Uuid::random());
clonedGroup->m_data = m_data; clonedGroup->m_data = m_data;
const QList<Entry*> entryList = entries(); if (!shallow) {
for (Entry* entry : entryList) { const QList<Entry*> entryList = entries();
Entry* clonedEntry = entry->clone(entryFlags); for (Entry* entry : entryList) {
clonedEntry->setGroup(clonedGroup); Entry* clonedEntry = entry->clone(entryFlags);
} clonedEntry->setGroup(clonedGroup);
}
const QList<Group*> childrenGroups = children(); const QList<Group*> childrenGroups = children();
for (Group* groupChild : childrenGroups) { for (Group* groupChild : childrenGroups) {
Group* clonedGroupChild = groupChild->clone(entryFlags); Group* clonedGroupChild = groupChild->clone(entryFlags);
clonedGroupChild->setParent(clonedGroup); clonedGroupChild->setParent(clonedGroup);
}
} }
clonedGroup->setUpdateTimeinfo(true); clonedGroup->setUpdateTimeinfo(true);
@ -829,8 +848,7 @@ void Group::markOlderEntry(Entry* entry)
{ {
entry->attributes()->set( entry->attributes()->set(
"merged", "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 bool Group::resolveSearchingEnabled() const
@ -880,32 +898,34 @@ void Group::resolveConflict(Entry* existingEntry, Entry* otherEntry)
Entry* clonedEntry; Entry* clonedEntry;
switch(mergeMode()) { switch (mergeMode()) {
case KeepBoth: case KeepBoth:
// if one entry is newer, create a clone and add it to the group // if one entry is newer, create a clone and add it to the group
if (timeExisting > timeOther) { if (timeExisting > timeOther) {
clonedEntry = otherEntry->clone(Entry::CloneNoFlags); clonedEntry = otherEntry->clone(Entry::CloneNewUuid);
clonedEntry->setGroup(this); clonedEntry->setGroup(this);
markOlderEntry(clonedEntry); markOlderEntry(clonedEntry);
} else if (timeExisting < timeOther) { } else if (timeExisting < timeOther) {
clonedEntry = otherEntry->clone(Entry::CloneNoFlags); clonedEntry = otherEntry->clone(Entry::CloneNewUuid);
clonedEntry->setGroup(this); clonedEntry->setGroup(this);
markOlderEntry(existingEntry); markOlderEntry(existingEntry);
} }
break; break;
case KeepNewer: case KeepNewer:
if (timeExisting < timeOther) { if (timeExisting < timeOther) {
// only if other entry is newer, replace existing one qDebug("Updating entry %s.", qPrintable(existingEntry->title()));
removeEntry(existingEntry); // only if other entry is newer, replace existing one
addEntry(otherEntry->clone(Entry::CloneNoFlags)); Group* currentGroup = existingEntry->group();
} currentGroup->removeEntry(existingEntry);
otherEntry->clone(Entry::CloneNoFlags)->setGroup(currentGroup);
}
break; break;
case KeepExisting: case KeepExisting:
break; break;
default: default:
// do nothing // do nothing
break; break;
} }
} }
@ -928,5 +948,4 @@ QStringList Group::locate(QString locateTerm, QString currentPath)
} }
return response; return response;
} }

View File

@ -117,13 +117,14 @@ public:
QList<Group*> groupsRecursive(bool includeSelf); QList<Group*> groupsRecursive(bool includeSelf);
QSet<Uuid> customIconsRecursive() const; QSet<Uuid> 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 * The exceptions are that the returned group doesn't have a parent group
* and all TimeInfo attributes are set to the current time. * and all TimeInfo attributes are set to the current time.
* Note that you need to copy the custom icons manually when inserting the * Note that you need to copy the custom icons manually when inserting the
* new group into another database. * 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 copyDataFrom(const Group* other);
void merge(const Group* other); void merge(const Group* other);
QString print(bool recursive = false, int depth = 0); QString print(bool recursive = false, int depth = 0);

View File

@ -158,6 +158,9 @@ endif()
add_unit_test(NAME testentry SOURCES TestEntry.cpp add_unit_test(NAME testentry SOURCES TestEntry.cpp
LIBS ${TEST_LIBRARIES}) LIBS ${TEST_LIBRARIES})
add_unit_test(NAME testmerge SOURCES TestMerge.cpp
LIBS ${TEST_LIBRARIES})
add_unit_test(NAME testtotp SOURCES TestTotp.cpp add_unit_test(NAME testtotp SOURCES TestTotp.cpp
LIBS ${TEST_LIBRARIES}) LIBS ${TEST_LIBRARIES})

View File

@ -452,123 +452,6 @@ void TestGroup::testCopyCustomIcons()
delete dbSource; 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() void TestGroup::testFindEntry()
{ {
Database* db = new Database(); Database* db = new Database();

View File

@ -35,17 +35,10 @@ private slots:
void testCopyCustomIcon(); void testCopyCustomIcon();
void testClone(); void testClone();
void testCopyCustomIcons(); void testCopyCustomIcons();
void testMerge();
void testMergeConflict();
void testMergeDatabase();
void testMergeConflictKeepBoth();
void testFindEntry(); void testFindEntry();
void testFindGroupByPath(); void testFindGroupByPath();
void testPrint(); void testPrint();
void testLocate(); void testLocate();
private:
Database* createMergeTestDatabase();
}; };
#endif // KEEPASSX_TESTGROUP_H #endif // KEEPASSX_TESTGROUP_H

445
tests/TestMerge.cpp Normal file
View File

@ -0,0 +1,445 @@
/*
* 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 <QDebug>
#include <QTest>
#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*>("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);
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;
}

46
tests/TestMerge.h Normal file
View File

@ -0,0 +1,46 @@
/*
* 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/>.
*/
#ifndef KEEPASSX_TESTMERGE_H
#define KEEPASSX_TESTMERGE_H
#include "core/Database.h"
#include <QObject>
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