mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-10-01 01:26:01 -04:00
Feature : Update entries across groups when merging (#807)
* Feature : Update entries across groups when merging * Styling
This commit is contained in:
parent
321d4e9c87
commit
1220b7d501
1
.gitignore
vendored
1
.gitignore
vendored
@ -6,3 +6,4 @@ release*/
|
|||||||
*.kdev4
|
*.kdev4
|
||||||
|
|
||||||
\.vscode/
|
\.vscode/
|
||||||
|
*.swp
|
||||||
|
@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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})
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
@ -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
445
tests/TestMerge.cpp
Normal 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
46
tests/TestMerge.h
Normal 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
|
Loading…
Reference in New Issue
Block a user