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 000000000..f45929de2 Binary files /dev/null and b/tests/data/MergeDatabase.kdbx differ diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index b44491323..b25a09fdc 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -29,6 +29,7 @@ #include #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();