From e25cd9ba48859322527aee78c4bcd60325487b76 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Mon, 7 Nov 2016 22:37:42 -0500 Subject: [PATCH] Add Merge database utility function (#47) Thank you to @TheZ3ro and @monomon for there major contributions to this PR! --- src/core/Database.cpp | 6 ++ src/core/Database.h | 1 + src/core/Group.cpp | 113 ++++++++++++++++++++++++++++++++ src/core/Group.h | 9 +++ src/gui/DatabaseTabWidget.cpp | 16 +++++ src/gui/DatabaseTabWidget.h | 2 + src/gui/DatabaseWidget.cpp | 53 +++++++++++++++ src/gui/DatabaseWidget.h | 10 ++- src/gui/MainWindow.cpp | 6 ++ src/gui/MainWindow.ui | 6 ++ src/http/OptionDialog.ui | 4 +- tests/TestGroup.cpp | 118 ++++++++++++++++++++++++++++++++++ tests/TestGroup.h | 8 +++ tests/data/MergeDatabase.kdbx | Bin 0 -> 15150 bytes tests/gui/TestGui.cpp | 32 +++++++++ tests/gui/TestGui.h | 1 + 16 files changed, 382 insertions(+), 3 deletions(-) create mode 100644 tests/data/MergeDatabase.kdbx diff --git a/src/core/Database.cpp b/src/core/Database.cpp index 9e01d3bc..6dc971b3 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -282,6 +282,12 @@ void Database::recycleGroup(Group* group) } } +void Database::merge(const Database* other) +{ + m_rootGroup->merge(other->rootGroup()); + Q_EMIT modified(); +} + void Database::setEmitModified(bool value) { if (m_emitModified && !value) { diff --git a/src/core/Database.h b/src/core/Database.h index 6fde3c60..6d2237d4 100644 --- a/src/core/Database.h +++ b/src/core/Database.h @@ -105,6 +105,7 @@ public: void recycleGroup(Group* group); void setEmitModified(bool value); void copyAttributesFrom(const Database* other); + void merge(const Database* other); /** * Returns a unique id that is only valid as long as the Database exists. diff --git a/src/core/Group.cpp b/src/core/Group.cpp index 325ef946..70260170 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -32,6 +32,7 @@ Group::Group() m_data.isExpanded = true; m_data.autoTypeEnabled = Inherit; m_data.searchingEnabled = Inherit; + m_data.mergeMode = ModeInherit; } Group::~Group() @@ -196,6 +197,19 @@ Group::TriState Group::searchingEnabled() const return m_data.searchingEnabled; } +Group::MergeMode Group::mergeMode() const +{ + if (m_data.mergeMode == Group::MergeMode::ModeInherit) { + if (m_parent) { + return m_parent->mergeMode(); + } else { + return Group::MergeMode::KeepNewer; // fallback + } + } else { + return m_data.mergeMode; + } +} + Entry* Group::lastTopVisibleEntry() const { return m_lastTopVisibleEntry; @@ -303,6 +317,11 @@ void Group::setExpiryTime(const QDateTime& dateTime) } } +void Group::setMergeMode(MergeMode newMode) +{ + set(m_data.mergeMode, newMode); +} + Group* Group::parentGroup() { return m_parent; @@ -440,6 +459,18 @@ QList Group::entriesRecursive(bool includeHistoryItems) const return entryList; } +Entry* Group::findEntry(const Uuid& uuid) +{ + Q_ASSERT(!uuid.isNull()); + for (Entry* entry : asConst(m_entries)) { + if (entry->uuid() == uuid) { + return entry; + } + } + + return nullptr; +} + QList Group::groupsRecursive(bool includeSelf) const { QList groupList; @@ -490,6 +521,44 @@ QSet Group::customIconsRecursive() const return result; } +void Group::merge(const Group* other) +{ + // merge entries + const QList dbEntries = other->entries(); + for (Entry* entry : dbEntries) { + // entries are searched by uuid + if (!findEntry(entry->uuid())) { + entry->clone(Entry::CloneNoFlags)->setGroup(this); + } else { + resolveConflict(this->findEntry(entry->uuid()), entry); + } + } + + // merge groups (recursively) + const QList dbChildren = other->children(); + for (Group* group : dbChildren) { + // groups are searched by name instead of uuid + if (this->findChildByName(group->name())) { + this->findChildByName(group->name())->merge(group); + } else { + group->setParent(this); + } + } + + Q_EMIT modified(); +} + +Group* Group::findChildByName(const QString& name) +{ + for (Group* group : asConst(m_children)) { + if (group->name() == name) { + return group; + } + } + + return nullptr; +} + Group* Group::clone(Entry::CloneFlags entryFlags) const { Group* clonedGroup = new Group(); @@ -624,6 +693,14 @@ void Group::recCreateDelObjects() } } +void Group::markOlderEntry(Entry* entry) +{ + entry->attributes()->set( + "merged", + QString("older entry merged from database \"%1\"").arg(entry->group()->database()->metadata()->name()) + ); +} + bool Group::resolveSearchingEnabled() const { switch (m_data.searchingEnabled) { @@ -663,3 +740,39 @@ bool Group::resolveAutoTypeEnabled() const return false; } } + +void Group::resolveConflict(Entry* existingEntry, Entry* otherEntry) +{ + const QDateTime timeExisting = existingEntry->timeInfo().lastModificationTime(); + const QDateTime timeOther = otherEntry->timeInfo().lastModificationTime(); + + Entry* clonedEntry; + + switch(this->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); + this->markOlderEntry(clonedEntry); + } else if (timeExisting < timeOther) { + clonedEntry = otherEntry->clone(Entry::CloneNoFlags); + clonedEntry->setGroup(this); + this->markOlderEntry(existingEntry); + } + break; + case KeepNewer: + if (timeExisting < timeOther) { + // only if other entry is newer, replace existing one + this->removeEntry(existingEntry); + this->addEntry(otherEntry); + } + + break; + case KeepExisting: + break; + default: + // do nothing + break; + } +} diff --git a/src/core/Group.h b/src/core/Group.h index 3881ed24..025814b6 100644 --- a/src/core/Group.h +++ b/src/core/Group.h @@ -34,6 +34,7 @@ class Group : public QObject public: enum TriState { Inherit, Enable, Disable }; + enum MergeMode { ModeInherit, KeepBoth, KeepNewer, KeepExisting }; struct GroupData { @@ -46,6 +47,7 @@ public: QString defaultAutoTypeSequence; Group::TriState autoTypeEnabled; Group::TriState searchingEnabled; + Group::MergeMode mergeMode; }; Group(); @@ -66,6 +68,7 @@ public: QString defaultAutoTypeSequence() const; Group::TriState autoTypeEnabled() const; Group::TriState searchingEnabled() const; + Group::MergeMode mergeMode() const; bool resolveSearchingEnabled() const; bool resolveAutoTypeEnabled() const; Entry* lastTopVisibleEntry() const; @@ -74,6 +77,8 @@ public: static const int DefaultIconNumber; static const int RecycleBinIconNumber; + Entry* findEntry(const Uuid& uuid); + Group* findChildByName(const QString& name); void setUuid(const Uuid& uuid); void setName(const QString& name); void setNotes(const QString& notes); @@ -87,6 +92,7 @@ public: void setLastTopVisibleEntry(Entry* entry); void setExpires(bool value); void setExpiryTime(const QDateTime& dateTime); + void setMergeMode(MergeMode newMode); void setUpdateTimeinfo(bool value); @@ -113,6 +119,7 @@ public: */ Group* clone(Entry::CloneFlags entryFlags = Entry::CloneNewUuid | Entry::CloneResetTimeInfo) const; void copyDataFrom(const Group* other); + void merge(const Group* other); Q_SIGNALS: void dataChanged(Group* group); @@ -142,6 +149,8 @@ private: void addEntry(Entry* entry); void removeEntry(Entry* entry); void setParent(Database* db); + void markOlderEntry(Entry* entry); + void resolveConflict(Entry* existingEntry, Entry* otherEntry); void recSetDatabase(Database* db); void cleanupParent(); diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index a5c5748c..6e8a7b74 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -24,6 +24,7 @@ #include "autotype/AutoType.h" #include "core/Config.h" +#include "core/Global.h" #include "core/Database.h" #include "core/Group.h" #include "core/Metadata.h" @@ -192,6 +193,21 @@ void DatabaseTabWidget::openDatabase(const QString& fileName, const QString& pw, } } +void DatabaseTabWidget::mergeDatabase() +{ + QString filter = QString("%1 (*.kdbx);;%2 (*)").arg(tr("KeePass 2 Database"), tr("All files")); + const QString fileName = fileDialog()->getOpenFileName(this, tr("Merge database"), QString(), + filter); + if (!fileName.isEmpty()) { + mergeDatabase(fileName); + } +} + +void DatabaseTabWidget::mergeDatabase(const QString& fileName) +{ + currentDatabaseWidget()->switchToOpenMergeDatabase(fileName); +} + void DatabaseTabWidget::importKeePass1Database() { QString fileName = fileDialog()->getOpenFileName(this, tr("Open KeePass 1 database"), QString(), diff --git a/src/gui/DatabaseTabWidget.h b/src/gui/DatabaseTabWidget.h index 618b48b1..7d095b56 100644 --- a/src/gui/DatabaseTabWidget.h +++ b/src/gui/DatabaseTabWidget.h @@ -55,6 +55,7 @@ public: ~DatabaseTabWidget(); void openDatabase(const QString& fileName, const QString& pw = QString(), const QString& keyFile = QString()); + void mergeDatabase(const QString& fileName); DatabaseWidget* currentDatabaseWidget(); bool hasLockableDatabases() const; @@ -63,6 +64,7 @@ public: public Q_SLOTS: void newDatabase(); void openDatabase(); + void mergeDatabase(); void importKeePass1Database(); bool saveDatabase(int index = -1); bool saveDatabaseAs(int index = -1); diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index d97fc2f8..e330d99d 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -118,6 +118,8 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) m_databaseSettingsWidget->setObjectName("databaseSettingsWidget"); m_databaseOpenWidget = new DatabaseOpenWidget(); m_databaseOpenWidget->setObjectName("databaseOpenWidget"); + m_databaseOpenMergeWidget = new DatabaseOpenWidget(); + m_databaseOpenMergeWidget->setObjectName("databaseOpenMergeWidget"); m_keepass1OpenWidget = new KeePass1OpenWidget(); m_keepass1OpenWidget->setObjectName("keepass1OpenWidget"); m_unlockDatabaseWidget = new UnlockDatabaseWidget(); @@ -129,6 +131,7 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) addWidget(m_databaseSettingsWidget); addWidget(m_historyEditEntryWidget); addWidget(m_databaseOpenWidget); + addWidget(m_databaseOpenMergeWidget); addWidget(m_keepass1OpenWidget); addWidget(m_unlockDatabaseWidget); @@ -147,6 +150,7 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) connect(m_changeMasterKeyWidget, SIGNAL(editFinished(bool)), SLOT(updateMasterKey(bool))); connect(m_databaseSettingsWidget, SIGNAL(editFinished(bool)), SLOT(switchToView(bool))); connect(m_databaseOpenWidget, SIGNAL(editFinished(bool)), SLOT(openDatabase(bool))); + connect(m_databaseOpenMergeWidget, SIGNAL(editFinished(bool)), SLOT(mergeDatabase(bool))); connect(m_keepass1OpenWidget, SIGNAL(editFinished(bool)), SLOT(openDatabase(bool))); connect(m_unlockDatabaseWidget, SIGNAL(editFinished(bool)), SLOT(unlockDatabase(bool))); connect(this, SIGNAL(currentChanged(int)), this, SLOT(emitCurrentModeChanged())); @@ -663,6 +667,28 @@ void DatabaseWidget::openDatabase(bool accepted) } } +void DatabaseWidget::mergeDatabase(bool accepted) +{ + if (accepted) { + if (!m_db) { + MessageBox::critical(this, tr("Error"), tr("No current database.")); + return; + } + + Database* srcDb = static_cast(sender())->database(); + + if (!srcDb) { + MessageBox::critical(this, tr("Error"), tr("No source database, nothing to do.")); + return; + } + + m_db->merge(srcDb); + } + + setCurrentWidget(m_mainWidget); + Q_EMIT databaseMerged(m_db); +} + void DatabaseWidget::unlockDatabase(bool accepted) { if (!accepted) { @@ -745,6 +771,19 @@ void DatabaseWidget::switchToOpenDatabase(const QString& fileName, const QString m_databaseOpenWidget->enterKey(password, keyFile); } +void DatabaseWidget::switchToOpenMergeDatabase(const QString& fileName) +{ + m_databaseOpenMergeWidget->load(fileName); + setCurrentWidget(m_databaseOpenMergeWidget); +} + +void DatabaseWidget::switchToOpenMergeDatabase(const QString& fileName, const QString& password, + const QString& keyFile) +{ + switchToOpenMergeDatabase(fileName); + m_databaseOpenMergeWidget->enterKey(password, keyFile); +} + void DatabaseWidget::switchToImportKeepass1(const QString& fileName) { updateFilename(fileName); @@ -856,6 +895,12 @@ bool DatabaseWidget::isInSearchMode() const return m_entryView->inEntryListMode(); } +Group* DatabaseWidget::currentGroup() const +{ + return isInSearchMode() ? m_lastGroup + : m_groupView->currentGroup(); +} + void DatabaseWidget::clearLastGroup(Group* group) { if (group) { @@ -956,3 +1001,11 @@ bool DatabaseWidget::currentEntryHasNotes() } return !currentEntry->notes().isEmpty(); } + +GroupView* DatabaseWidget::groupView() { + return m_groupView; +} + +EntryView* DatabaseWidget::entryView() { + return m_entryView; +} diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index 57142903..8aa773fa 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -62,6 +62,7 @@ public: bool canDeleteCurrentGroup() const; bool isInSearchMode() const; QString getCurrentSearch(); + Group* currentGroup() const; int addWidget(QWidget* w); void setCurrentIndex(int index); void setCurrentWidget(QWidget* widget); @@ -83,6 +84,8 @@ public: bool currentEntryHasPassword(); bool currentEntryHasUrl(); bool currentEntryHasNotes(); + GroupView* groupView(); + EntryView* entryView(); Q_SIGNALS: void closeRequest(); @@ -90,6 +93,7 @@ Q_SIGNALS: void groupChanged(); void entrySelectionChanged(); void databaseChanged(Database* newDb); + void databaseMerged(Database* mergedDb); void groupContextMenuRequested(const QPoint& globalPos); void entryContextMenuRequested(const QPoint& globalPos); void unlockedDatabase(); @@ -116,12 +120,15 @@ public Q_SLOTS: void openUrlForEntry(Entry* entry); void createGroup(); void deleteGroup(); + void switchToView(bool accepted); void switchToEntryEdit(); void switchToGroupEdit(); void switchToMasterKeyChange(); void switchToDatabaseSettings(); void switchToOpenDatabase(const QString& fileName); void switchToOpenDatabase(const QString& fileName, const QString& password, const QString& keyFile); + void switchToOpenMergeDatabase(const QString& fileName); + void switchToOpenMergeDatabase(const QString& fileName, const QString& password, const QString& keyFile); void switchToImportKeepass1(const QString& fileName); // Search related slots void search(const QString& searchtext); @@ -132,7 +139,6 @@ public Q_SLOTS: private Q_SLOTS: void entryActivationSignalReceived(Entry* entry, EntryModel::ModelColumn column); void switchBackToEntryEdit(); - void switchToView(bool accepted); void switchToHistoryView(Entry* entry); void switchToEntryEdit(Entry* entry); void switchToEntryEdit(Entry* entry, bool create); @@ -141,6 +147,7 @@ private Q_SLOTS: void emitEntryContextMenuRequested(const QPoint& pos); void updateMasterKey(bool accepted); void openDatabase(bool accepted); + void mergeDatabase(bool accepted); void unlockDatabase(bool accepted); void emitCurrentModeChanged(); void clearLastGroup(Group* group); @@ -158,6 +165,7 @@ private: ChangeMasterKeyWidget* m_changeMasterKeyWidget; DatabaseSettingsWidget* m_databaseSettingsWidget; DatabaseOpenWidget* m_databaseOpenWidget; + DatabaseOpenWidget* m_databaseOpenMergeWidget; KeePass1OpenWidget* m_keepass1OpenWidget; UnlockDatabaseWidget* m_unlockDatabaseWidget; QSplitter* m_splitter; diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index caac3797..54725a52 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -213,6 +213,8 @@ MainWindow::MainWindow() SLOT(saveDatabaseAs())); connect(m_ui->actionDatabaseClose, SIGNAL(triggered()), m_ui->tabWidget, SLOT(closeDatabase())); + connect(m_ui->actionDatabaseMerge, SIGNAL(triggered()), m_ui->tabWidget, + SLOT(mergeDatabase())); connect(m_ui->actionChangeMasterKey, SIGNAL(triggered()), m_ui->tabWidget, SLOT(changeMasterKey())); connect(m_ui->actionChangeDatabaseSettings, SIGNAL(triggered()), m_ui->tabWidget, @@ -378,6 +380,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) m_ui->actionDatabaseSave->setEnabled(true); m_ui->actionDatabaseSaveAs->setEnabled(true); m_ui->actionExportCsv->setEnabled(true); + m_ui->actionDatabaseMerge->setEnabled(m_ui->tabWidget->currentIndex() != -1); m_searchWidgetAction->setEnabled(true); break; @@ -405,6 +408,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) m_ui->actionDatabaseSave->setEnabled(false); m_ui->actionDatabaseSaveAs->setEnabled(false); m_ui->actionExportCsv->setEnabled(false); + m_ui->actionDatabaseMerge->setEnabled(false); m_searchWidgetAction->setEnabled(false); break; @@ -437,6 +441,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) m_ui->actionDatabaseSaveAs->setEnabled(false); m_ui->actionDatabaseClose->setEnabled(false); m_ui->actionExportCsv->setEnabled(false); + m_ui->actionDatabaseMerge->setEnabled(false); m_searchWidgetAction->setEnabled(false); } @@ -446,6 +451,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) m_ui->actionDatabaseOpen->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); m_ui->menuRecentDatabases->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); m_ui->actionImportKeePass1->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); + m_ui->actionDatabaseMerge->setEnabled(inDatabaseTabWidget); m_ui->actionRepairDatabase->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); m_ui->actionLockDatabases->setEnabled(m_ui->tabWidget->hasLockableDatabases()); diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index c9699ab2..3cc2a67e 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -119,6 +119,7 @@ + @@ -243,6 +244,11 @@ &New database + + + Merge from KeePassX database + + false diff --git a/src/http/OptionDialog.ui b/src/http/OptionDialog.ui index a230f2ad..326507d5 100644 --- a/src/http/OptionDialog.ui +++ b/src/http/OptionDialog.ui @@ -201,7 +201,7 @@ Only entries with the same scheme (http://, https://, ftp://, ...) are returned< - + @@ -225,7 +225,7 @@ Only entries with the same scheme (http://, https://, ftp://, ...) are returned< - + diff --git a/tests/TestGroup.cpp b/tests/TestGroup.cpp index e271abfc..e87e6ced 100644 --- a/tests/TestGroup.cpp +++ b/tests/TestGroup.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include "core/Database.h" @@ -449,3 +450,120 @@ void TestGroup::testCopyCustomIcons() delete dbTarget; 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; +} diff --git a/tests/TestGroup.h b/tests/TestGroup.h index c612a3ac..4a891ae6 100644 --- a/tests/TestGroup.h +++ b/tests/TestGroup.h @@ -19,6 +19,7 @@ #define KEEPASSX_TESTGROUP_H #include +#include "core/Database.h" class TestGroup : public QObject { @@ -33,6 +34,13 @@ private Q_SLOTS: void testCopyCustomIcon(); void testClone(); void testCopyCustomIcons(); + void testMerge(); + void testMergeConflict(); + void testMergeDatabase(); + void testMergeConflictKeepBoth(); + +private: + Database* createMergeTestDatabase(); }; #endif // KEEPASSX_TESTGROUP_H diff --git a/tests/data/MergeDatabase.kdbx b/tests/data/MergeDatabase.kdbx new file mode 100644 index 0000000000000000000000000000000000000000..f45929de27c5bd3a879623ab351175917c85b06a GIT binary patch literal 15150 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 KWiOj9qs7dxo(k&$g=mPi;WD*t_-6> zy1-#MdL^Z%`RhFg!gCJy$UZmg@Ne%R&{TXFzMaF#DKs(vkbwWWBI}oKI8YucYOvb8 zbDHRHgr`e0&zA4s@WnVl3Vptjj(2u36XBBPjO{HhenahHdZOHzztp$b8*0$Mub_gx zz%zWi#ig^Xf%>kqkzICm*_-tYcKJFsAka3_q+~f?ml&2!rRxR=FquK*Ukbyo zFf!EoB^GHl&5x}{Jm^_S4A|K8A!Q3YSydQLon;Yy_iS?C%SO<$Q6nWN%M##=95gcl!ixWQ*`8QDm0~o|7yOy*7p3exJ?v z{bfR{_$Z1Is16vKi8elJ(`;Bt@1;aRw3qT{20pAn@0rbzND%Bu~r($*GIb=9Hn*Z zp#~XK>P2AZN@a3dpvB%UNK~61)x2{v&mHU|sJ{Py64y-@{yzY0OSjkSy+J1|`3SyT zRk4sp+XkMc!xqzom!e=Ge`Z58CUdDFO9ASGC=svPD>Odzoa&v*NyDcR2RoYf{+;|WWGYqy-t;~K2(LDC;W9aY)yhEj_R>!1j z^ZjC`ZF`D_V+#j)K2Tn0j9BnTA~5Bg1xl&R^_k`~&hEm6yBdK^!rqxP3Ds-BagqGp zG%>)_*uz#O*w6XJL&`~9&6#vqOeiHZ@;+uJrl}RA6xC7`*hKoN=;bg_6T@L2unbN9 zNbKyms?fKkpFOFb7pT3b;HCxFn)CrOw|envbKyEJt^WAb?OzU}Z@p3%Fh2F3J<6ui zGpyL5|HxjDt( z`2PAkQ^dQNt>KJt?=!Lq&A8JNX0c)tWa(z3SI0d6ALH2l>rs%*|8hec`@W9YRtYHh z+jy&y1h+TQs5Z_54j!~=bB9HP^d~H;DqL|K)HJtR5eSj|crP>3S92)j|9{9w3d(+% z$=d!!PWj;5O)0vt(Ypk^O)_aN^pMEnsVwc%$45p zQiJ4<95v1i?XOw!9y)Y0d+D3i%lDPD!hJNxUIu1)R`;{9(XP?Ctq2#}#dE{L!1K99$IiT*dnj8IW!JY$Cw{rE4&~RDid!NB0@qFDEn`#FMhiXo)6KR|jOv zRr`DqRi_ajCSv3X9Jzq!#uU~O^QS)%+FL*5%b9$gP62J0faVoB+hq~bmHrQ(?N;p+ ziSwkPfH=js6h51vc>zr=4$3L+pME{>@-@A5osnu^`)asIcjh#;puWT+fKghj(_Dl2 zjhFcg{n2JYQwK?I3JZT8_Pl!3;sLByVhL!Dp~g83VRx&5GGB6ZDud6r_F`qTxc za`kuDKYD3pgKH6$t#c+b7aY88^!5T{#6u2w&!;!xbn&va^A$1tp618fCL^JOUD{5>l(I5rfN5?CHmnDlPh ziSXoSuXWEsadD!Mx7!Q^d#%vk`y+4TMa@9)0C0zCDTh-Y;$$ZWB{)gCX4xbn1^`6N zNzhm#>SBhD2z(NaZ$O<8UcQQ!P(-|ASJD9kEn*8QYqx*2sVvCr)%MgST+=5ke~Bo$ zqWDp?&X6_@nZlMYU?bLSvAXej?7F;+5$|y*92lVor?4)exps*$I-NOqfzD#EbvSvj zb)tB!F|no%GW;2IF|q;{E`(n ziq<8oh_6lyY #include #include +#include #include "config-keepassx-tests.h" #include "core/Config.h" @@ -107,6 +108,37 @@ void TestGui::cleanup() m_dbWidget = nullptr; } +void TestGui::testMergeDatabase() +{ + // this triggers a warning. Perhaps similar to https://bugreports.qt.io/browse/QTBUG-49623 ? + QSignalSpy dbMergeSpy(m_tabWidget->currentWidget(), SIGNAL(databaseMerged(Database*))); + + // set file to merge from + fileDialog()->setNextFileName(QString(KEEPASSX_TEST_DATA_DIR).append("/MergeDatabase.kdbx")); + triggerAction("actionDatabaseMerge"); + + QWidget* databaseOpenMergeWidget = m_mainWindow->findChild("databaseOpenMergeWidget"); + QLineEdit* editPasswordMerge = databaseOpenMergeWidget->findChild("editPassword"); + QVERIFY(editPasswordMerge->isVisible()); + + m_tabWidget->currentDatabaseWidget()->setCurrentWidget(databaseOpenMergeWidget); + + QTest::keyClicks(editPasswordMerge, "a"); + QTest::keyClick(editPasswordMerge, Qt::Key_Enter); + + QTRY_COMPARE(dbMergeSpy.count(), 1); + QTRY_VERIFY(m_tabWidget->tabText(m_tabWidget->currentIndex()).contains("*")); + + m_db = m_tabWidget->currentDatabaseWidget()->database(); + + // there are seven child groups of the root group + QCOMPARE(m_db->rootGroup()->children().size(), 7); + // the merged group should contain an entry + QCOMPARE(m_db->rootGroup()->children().at(6)->entries().size(), 1); + // the General group contains one entry merged from the other db + QCOMPARE(m_db->rootGroup()->findChildByName("General")->entries().size(), 1); +} + void TestGui::testTabs() { QCOMPARE(m_tabWidget->count(), 1); diff --git a/tests/gui/TestGui.h b/tests/gui/TestGui.h index 72e3f405..82ffc185 100644 --- a/tests/gui/TestGui.h +++ b/tests/gui/TestGui.h @@ -38,6 +38,7 @@ private Q_SLOTS: void cleanup(); void cleanupTestCase(); + void testMergeDatabase(); void testTabs(); void testEditEntry(); void testAddEntry();