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 9e01d3bc0..6dc971b33 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 6fde3c601..6d2237d4c 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 325ef9467..70260170a 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 3881ed246..025814b6c 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 a5c5748c1..6e8a7b744 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 618b48b1c..7d095b560 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 d97fc2f85..e330d99d0 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 571429036..8aa773fa2 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 caac37974..54725a52a 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 c9699ab2c..3cc2a67ea 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 a230f2ad7..326507d51 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 e271abfc0..e87e6cedc 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 c612a3ac6..4a891ae6f 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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 Nf)+_^`zY6Z3IH&)Xc_ #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 72e3f4056..82ffc1850 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();