From c1e9f45df9f21b7697241037643770a2862bb7ef Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Sun, 30 Sep 2018 08:45:06 -0400 Subject: [PATCH] Introduce synchronize merge method * Create history-based merging that keeps older data in history instead of discarding or deleting it * Extract merge logic into the Merger class * Allows special merge behavior * Improve handling of deletion and changes on groups * Enable basic change tracking while merging * Prevent unintended timestamp changes while merging * Handle differences in timestamp precision * Introduce comparison operators to allow for more sophisticated comparisons (ignore special properties, ...) * Introduce Clock class to handle datetime across the app Merge Strategies: * Default (use inherited/fallback method) * Duplicate (duplicate conflicting nodes, apply all deletions) * KeepLocal (use local values, but apply all deletions) * KeepRemote (use remote values, but apply all deletions) * KeepNewer (merge history only) * Synchronize (merge history, newest value stays on top, apply all deletions) --- src/CMakeLists.txt | 3 + src/autotype/AutoType.cpp | 7 +- src/browser/BrowserService.cpp | 6 +- src/cli/Merge.cpp | 4 +- src/core/AutoTypeAssociations.cpp | 18 + src/core/AutoTypeAssociations.h | 3 + src/core/Clock.cpp | 109 ++ src/core/Clock.h | 58 + src/core/Compare.cpp | 38 + src/core/Compare.h | 90 ++ src/core/Database.cpp | 62 +- src/core/Database.h | 10 +- src/core/Entry.cpp | 169 ++- src/core/Entry.h | 15 +- src/core/Group.cpp | 240 ++- src/core/Group.h | 32 +- src/core/Merger.cpp | 613 ++++++++ src/core/Merger.h | 72 + src/core/Metadata.cpp | 5 +- src/core/TimeInfo.cpp | 39 +- src/core/TimeInfo.h | 7 + src/core/Tools.h | 1 - src/format/KdbxXmlReader.cpp | 5 +- src/gui/DatabaseWidget.cpp | 11 +- src/gui/TotpDialog.cpp | 5 +- src/gui/csvImport/CsvImportWidget.cpp | 6 +- .../DatabaseSettingsWidgetGeneral.cpp | 3 +- src/gui/entry/EditEntryWidget.cpp | 5 +- src/gui/group/EditGroupWidget.h | 11 +- src/totp/totp.cpp | 5 +- tests/CMakeLists.txt | 12 +- tests/TestEntry.cpp | 65 +- tests/TestGlobal.h | 10 - tests/TestGroup.cpp | 25 +- tests/TestGroup.h | 2 + tests/TestKeePass2Format.cpp | 41 +- tests/TestMerge.cpp | 1347 +++++++++++++---- tests/TestMerge.h | 34 +- tests/TestModified.cpp | 24 +- tests/TestModified.h | 2 + tests/gui/TestGui.cpp | 12 +- tests/stub/TestClock.cpp | 86 ++ tests/stub/TestClock.h | 50 + 43 files changed, 2777 insertions(+), 585 deletions(-) create mode 100644 src/core/Clock.cpp create mode 100644 src/core/Clock.h create mode 100644 src/core/Compare.cpp create mode 100644 src/core/Compare.h create mode 100644 src/core/Merger.cpp create mode 100644 src/core/Merger.h create mode 100644 tests/stub/TestClock.cpp create mode 100644 tests/stub/TestClock.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 76691de12..3621067e8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -40,6 +40,7 @@ set(keepassx_SOURCES core/AutoTypeAssociations.cpp core/AsyncTask.h core/AutoTypeMatch.cpp + core/Compare.cpp core/Config.cpp core/CsvParser.cpp core/CustomData.cpp @@ -54,6 +55,7 @@ set(keepassx_SOURCES core/Group.cpp core/InactivityTimer.cpp core/ListDeleter.h + core/Merger.cpp core/Metadata.cpp core/PasswordGenerator.cpp core/PassphraseGenerator.cpp @@ -64,6 +66,7 @@ set(keepassx_SOURCES core/ScreenLockListenerPrivate.cpp core/TimeDelta.cpp core/TimeInfo.cpp + core/Clock.cpp core/Tools.cpp core/Translator.cpp core/Base32.h diff --git a/src/autotype/AutoType.cpp b/src/autotype/AutoType.cpp index aaa742a09..89c24e55e 100644 --- a/src/autotype/AutoType.cpp +++ b/src/autotype/AutoType.cpp @@ -756,10 +756,9 @@ bool AutoType::verifyAutoTypeSyntax(const QString& sequence) } } else if (AutoType::checkHighRepetition(sequence)) { QMessageBox::StandardButton reply; - reply = QMessageBox::question(nullptr, - tr("Auto-Type"), - tr("This Auto-Type command contains arguments which are " - "repeated very often. Do you really want to proceed?")); + reply = + QMessageBox::question(nullptr, tr("Auto-Type"), tr("This Auto-Type command contains arguments which are " + "repeated very often. Do you really want to proceed?")); if (reply == QMessageBox::No) { return false; diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index bba651d56..086c15062 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -114,7 +114,7 @@ QString BrowserService::getDatabaseRootUuid() return QString(); } - return QString::fromLatin1(rootGroup->uuid().toRfc4122().toHex()); + return rootGroup->uuidToHex(); } QString BrowserService::getDatabaseRecycleBinUuid() @@ -128,7 +128,7 @@ QString BrowserService::getDatabaseRecycleBinUuid() if (!recycleBin) { return QString(); } - return QString::fromLatin1(recycleBin->uuid().toRfc4122().toHex()); + return recycleBin->uuidToHex(); } Entry* BrowserService::getConfigEntry(bool create) @@ -636,7 +636,7 @@ QJsonObject BrowserService::prepareEntry(const Entry* entry) res["login"] = entry->resolveMultiplePlaceholders(entry->username()); res["password"] = entry->resolveMultiplePlaceholders(entry->password()); res["name"] = entry->resolveMultiplePlaceholders(entry->title()); - res["uuid"] = entry->resolveMultiplePlaceholders(QString::fromLatin1(entry->uuid().toRfc4122().toHex())); + res["uuid"] = entry->resolveMultiplePlaceholders(entry->uuidToHex()); if (entry->hasTotp()) { res["totp"] = entry->totp(); diff --git a/src/cli/Merge.cpp b/src/cli/Merge.cpp index 8248c45a0..ea7e6636a 100644 --- a/src/cli/Merge.cpp +++ b/src/cli/Merge.cpp @@ -23,6 +23,7 @@ #include #include "core/Database.h" +#include "core/Merger.h" Merge::Merge() { @@ -82,7 +83,8 @@ int Merge::execute(const QStringList& arguments) return EXIT_FAILURE; } - db1->merge(db2); + Merger merger(db2, db1); + merger.merge(); QString errorMessage = db1->saveToFile(args.at(0)); if (!errorMessage.isEmpty()) { diff --git a/src/core/AutoTypeAssociations.cpp b/src/core/AutoTypeAssociations.cpp index 730e38ca1..a9ecc0db1 100644 --- a/src/core/AutoTypeAssociations.cpp +++ b/src/core/AutoTypeAssociations.cpp @@ -115,3 +115,21 @@ void AutoTypeAssociations::clear() { m_associations.clear(); } + +bool AutoTypeAssociations::operator==(const AutoTypeAssociations& other) const +{ + if (m_associations.count() != other.m_associations.count()) { + return false; + } + for (int i = 0; i < m_associations.count(); ++i) { + if (m_associations[i] != other.m_associations[i]) { + return false; + } + } + return true; +} + +bool AutoTypeAssociations::operator!=(const AutoTypeAssociations& other) const +{ + return !(*this == other); +} diff --git a/src/core/AutoTypeAssociations.h b/src/core/AutoTypeAssociations.h index 31e58cda0..17d5c3bcd 100644 --- a/src/core/AutoTypeAssociations.h +++ b/src/core/AutoTypeAssociations.h @@ -46,6 +46,9 @@ public: int associationsSize() const; void clear(); + bool operator==(const AutoTypeAssociations& other) const; + bool operator!=(const AutoTypeAssociations& other) const; + private: QList m_associations; diff --git a/src/core/Clock.cpp b/src/core/Clock.cpp new file mode 100644 index 000000000..02c2ae1bc --- /dev/null +++ b/src/core/Clock.cpp @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2018 KeePassXC Team + * + * 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 . + */ +#include "Clock.h" + +QSharedPointer Clock::m_instance = QSharedPointer(); + +QDateTime Clock::currentDateTimeUtc() +{ + return instance().currentDateTimeUtcImpl(); +} + +QDateTime Clock::currentDateTime() +{ + return instance().currentDateTimeImpl(); +} + +uint Clock::currentSecondsSinceEpoch() +{ + return instance().currentDateTimeImpl().toTime_t(); +} + +QDateTime Clock::serialized(const QDateTime& dateTime) +{ + auto time = dateTime.time(); + if (time.isValid() && time.msec() != 0) { + return dateTime.addMSecs(-time.msec()); + } + return dateTime; +} + +QDateTime Clock::datetimeUtc(int year, int month, int day, int hour, int min, int second) +{ + return QDateTime(QDate(year, month, day), QTime(hour, min, second), Qt::UTC); +} + +QDateTime Clock::datetime(int year, int month, int day, int hour, int min, int second) +{ + return QDateTime(QDate(year, month, day), QTime(hour, min, second), Qt::LocalTime); +} + +QDateTime Clock::datetimeUtc(qint64 msecSinceEpoch) +{ + return QDateTime::fromMSecsSinceEpoch(msecSinceEpoch, Qt::UTC); +} + +QDateTime Clock::datetime(qint64 msecSinceEpoch) +{ + return QDateTime::fromMSecsSinceEpoch(msecSinceEpoch, Qt::LocalTime); +} + +QDateTime Clock::parse(const QString& text, Qt::DateFormat format) +{ + return QDateTime::fromString(text, format); +} + +QDateTime Clock::parse(const QString& text, const QString& format) +{ + return QDateTime::fromString(text, format); +} + +Clock::~Clock() +{ +} + +Clock::Clock() +{ +} + +QDateTime Clock::currentDateTimeUtcImpl() const +{ + return QDateTime::currentDateTimeUtc(); +} + +QDateTime Clock::currentDateTimeImpl() const +{ + return QDateTime::currentDateTime(); +} + +void Clock::resetInstance() +{ + m_instance.clear(); +} + +void Clock::setInstance(Clock* clock) +{ + m_instance = QSharedPointer(clock); +} + +const Clock& Clock::instance() +{ + if (!m_instance) { + m_instance = QSharedPointer(new Clock()); + } + return *m_instance; +} diff --git a/src/core/Clock.h b/src/core/Clock.h new file mode 100644 index 000000000..8f81b0961 --- /dev/null +++ b/src/core/Clock.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2018 KeePassXC Team + * + * 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 . + */ + +#ifndef KEEPASSXC_CLOCK_H +#define KEEPASSXC_CLOCK_H + +#include +#include + +class Clock +{ +public: + static QDateTime currentDateTimeUtc(); + static QDateTime currentDateTime(); + + static uint currentSecondsSinceEpoch(); + + static QDateTime serialized(const QDateTime& dateTime); + + static QDateTime datetimeUtc(int year, int month, int day, int hour, int min, int second); + static QDateTime datetime(int year, int month, int day, int hour, int min, int second); + + static QDateTime datetimeUtc(qint64 msecSinceEpoch); + static QDateTime datetime(qint64 msecSinceEpoch); + + static QDateTime parse(const QString& text, Qt::DateFormat format = Qt::TextDate); + static QDateTime parse(const QString& text, const QString& format); + + virtual ~Clock(); + +protected: + Clock(); + virtual QDateTime currentDateTimeUtcImpl() const; + virtual QDateTime currentDateTimeImpl() const; + + static void resetInstance(); + static void setInstance(Clock* clock); + static const Clock& instance(); + +private: + static QSharedPointer m_instance; +}; + +#endif // KEEPASSX_ENTRY_H diff --git a/src/core/Compare.cpp b/src/core/Compare.cpp new file mode 100644 index 000000000..12e5029b7 --- /dev/null +++ b/src/core/Compare.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2018 KeePassXC Team + * + * 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 . + */ +#include "Compare.h" + +#include + +bool operator<(const QColor& lhs, const QColor& rhs) +{ + const QColor adaptedLhs = lhs.toCmyk(); + const QColor adaptedRhs = rhs.toCmyk(); + const int iCyan = compare(adaptedLhs.cyanF(), adaptedRhs.cyanF()); + if (iCyan != 0) { + return iCyan; + } + const int iMagenta = compare(adaptedLhs.magentaF(), adaptedRhs.magentaF()); + if (iMagenta != 0) { + return iMagenta; + } + const int iYellow = compare(adaptedLhs.yellowF(), adaptedRhs.yellowF()); + if (iYellow != 0) { + return iYellow; + } + return compare(adaptedLhs.blackF(), adaptedRhs.blackF()) < 0; +} diff --git a/src/core/Compare.h b/src/core/Compare.h new file mode 100644 index 000000000..5124caf6e --- /dev/null +++ b/src/core/Compare.h @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2018 KeePassXC Team + * + * 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 . + */ + +#ifndef KEEPASSXC_COMPARE_H +#define KEEPASSXC_COMPARE_H + +#include + +#include "core/Clock.h" + +enum CompareItemOption +{ + CompareItemDefault = 0, + CompareItemIgnoreMilliseconds = 0x4, + CompareItemIgnoreStatistics = 0x8, + CompareItemIgnoreDisabled = 0x10, + CompareItemIgnoreHistory = 0x20, + CompareItemIgnoreLocation = 0x40, +}; +Q_DECLARE_FLAGS(CompareItemOptions, CompareItemOption) +Q_DECLARE_OPERATORS_FOR_FLAGS(CompareItemOptions) + +class QColor; +/*! + * \return true when both color match + * + * Comparison converts both into the cmyk-model + */ +bool operator<(const QColor& lhs, const QColor& rhs); + +template inline short compareGeneric(const Type& lhs, const Type& rhs, CompareItemOptions) +{ + if (lhs != rhs) { + return lhs < rhs ? -1 : +1; + } + return 0; +} + +template +inline short compare(const Type& lhs, const Type& rhs, CompareItemOptions options = CompareItemDefault) +{ + return compareGeneric(lhs, rhs, options); +} + +template <> inline short compare(const QDateTime& lhs, const QDateTime& rhs, CompareItemOptions options) +{ + if (!options.testFlag(CompareItemIgnoreMilliseconds)) { + return compareGeneric(lhs, rhs, options); + } + return compareGeneric(Clock::serialized(lhs), Clock::serialized(rhs), options); +} + +template +inline short compare(bool enabled, const Type& lhs, const Type& rhs, CompareItemOptions options = CompareItemDefault) +{ + if (!enabled) { + return 0; + } + return compare(lhs, rhs, options); +} + +template +inline short compare(bool lhsEnabled, + const Type& lhs, + bool rhsEnabled, + const Type& rhs, + CompareItemOptions options = CompareItemDefault) +{ + const short enabled = compareGeneric(lhsEnabled, rhsEnabled, options); + if (enabled == 0 && (!options.testFlag(CompareItemIgnoreDisabled) || (lhsEnabled && rhsEnabled))) { + return compare(lhs, rhs, options); + } + return enabled; +} + +#endif // KEEPASSX_COMPARE_H diff --git a/src/core/Database.cpp b/src/core/Database.cpp index bc0a1b302..5b7a3c07d 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -27,7 +27,9 @@ #include #include "cli/Utils.h" +#include "core/Clock.h" #include "core/Group.h" +#include "core/Merger.h" #include "core/Metadata.h" #include "crypto/kdf/AesKdf.h" #include "format/KeePass2.h" @@ -40,6 +42,7 @@ QHash Database::m_uuidMap; Database::Database() : m_metadata(new Metadata(this)) + , m_rootGroup(nullptr) , m_timer(new QTimer(this)) , m_emitModified(false) , m_uuid(QUuid::createUuid()) @@ -216,6 +219,39 @@ QList Database::deletedObjects() return m_deletedObjects; } +const QList& Database::deletedObjects() const +{ + return m_deletedObjects; +} + +bool Database::containsDeletedObject(const QUuid& uuid) const +{ + for (const DeletedObject& currentObject : m_deletedObjects) { + if (currentObject.uuid == uuid) { + return true; + } + } + return false; +} + +bool Database::containsDeletedObject(const DeletedObject& object) const +{ + for (const DeletedObject& currentObject : m_deletedObjects) { + if (currentObject.uuid == object.uuid) { + return true; + } + } + return false; +} + +void Database::setDeletedObjects(const QList& delObjs) +{ + if (m_deletedObjects == delObjs) { + return; + } + m_deletedObjects = delObjs; +} + void Database::addDeletedObject(const DeletedObject& delObj) { Q_ASSERT(delObj.deletionTime.timeSpec() == Qt::UTC); @@ -225,7 +261,7 @@ void Database::addDeletedObject(const DeletedObject& delObj) void Database::addDeletedObject(const QUuid& uuid) { DeletedObject delObj; - delObj.deletionTime = QDateTime::currentDateTimeUtc(); + delObj.deletionTime = Clock::currentDateTimeUtc(); delObj.uuid = uuid; addDeletedObject(delObj); @@ -303,7 +339,7 @@ bool Database::setKey(QSharedPointer key, bool updateChanged m_data.transformedMasterKey = transformedMasterKey; m_data.hasKey = true; if (updateChangedTime) { - m_metadata->setMasterKeyChanged(QDateTime::currentDateTimeUtc()); + m_metadata->setMasterKeyChanged(Clock::currentDateTimeUtc()); } if (oldTransformedMasterKey != m_data.transformedMasterKey) { @@ -401,21 +437,6 @@ void Database::emptyRecycleBin() } } -void Database::merge(const Database* other) -{ - m_rootGroup->merge(other->rootGroup()); - - for (const QUuid& customIconId : other->metadata()->customIcons().keys()) { - QImage customIcon = other->metadata()->customIcon(customIconId); - if (!this->metadata()->containsCustomIcon(customIconId)) { - qDebug() << QString("Adding custom icon %1 to database.").arg(customIconId.toString()); - this->metadata()->addCustomIcon(customIconId, customIcon); - } - } - - emit modified(); -} - void Database::setEmitModified(bool value) { if (m_emitModified && !value) { @@ -425,6 +446,11 @@ void Database::setEmitModified(bool value) m_emitModified = value; } +void Database::markAsModified() +{ + emit modified(); +} + const QUuid& Database::uuid() { return m_uuid; @@ -467,7 +493,6 @@ Database* Database::openDatabaseFile(const QString& fileName, QSharedPointer deletedObjects(); + const QList& deletedObjects() const; void addDeletedObject(const DeletedObject& delObj); void addDeletedObject(const QUuid& uuid); + bool containsDeletedObject(const QUuid& uuid) const; + bool containsDeletedObject(const DeletedObject& uuid) const; + void setDeletedObjects(const QList& delObjs); const QUuid& cipher() const; Database::CompressionAlgorithm compressionAlgo() const; @@ -112,7 +120,7 @@ public: void recycleGroup(Group* group); void emptyRecycleBin(); void setEmitModified(bool value); - void merge(const Database* other); + void markAsModified(); QString saveToFile(QString filePath, bool atomic = true, bool backup = false); /** diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index bc394b227..929447f9c 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -19,6 +19,7 @@ #include "config-keepassx.h" +#include "core/Clock.h" #include "core/Database.h" #include "core/DatabaseIcons.h" #include "core/Group.h" @@ -60,6 +61,7 @@ Entry::Entry() Entry::~Entry() { + setUpdateTimeinfo(false); if (m_group) { m_group->removeEntry(this); @@ -77,19 +79,23 @@ template inline bool Entry::set(T& property, const T& value) property = value; emit modified(); return true; - } else { - return false; } + return false; } void Entry::updateTimeinfo() { if (m_updateTimeinfo) { - m_data.timeInfo.setLastModificationTime(QDateTime::currentDateTimeUtc()); - m_data.timeInfo.setLastAccessTime(QDateTime::currentDateTimeUtc()); + m_data.timeInfo.setLastModificationTime(Clock::currentDateTimeUtc()); + m_data.timeInfo.setLastAccessTime(Clock::currentDateTimeUtc()); } } +bool Entry::canUpdateTimeinfo() const +{ + return m_updateTimeinfo; +} + void Entry::setUpdateTimeinfo(bool value) { m_updateTimeinfo = value; @@ -123,6 +129,11 @@ const QUuid& Entry::uuid() const return m_uuid; } +const QString Entry::uuidToHex() const +{ + return QString::fromLatin1(m_uuid.toRfc4122().toHex()); +} + QImage Entry::icon() const { if (m_data.customIcon.isNull()) { @@ -142,15 +153,13 @@ QPixmap Entry::iconPixmap() const { if (m_data.customIcon.isNull()) { return databaseIcons()->iconPixmap(m_data.iconNumber); - } else { - Q_ASSERT(database()); - - if (database()) { - return database()->metadata()->customIconPixmap(m_data.customIcon); - } else { - return QPixmap(); - } } + + Q_ASSERT(database()); + if (database()) { + return database()->metadata()->customIconPixmap(m_data.customIcon); + } + return QPixmap(); } QPixmap Entry::iconScaledPixmap() const @@ -158,11 +167,9 @@ QPixmap Entry::iconScaledPixmap() const if (m_data.customIcon.isNull()) { // built-in icons are 16x16 so don't need to be scaled return databaseIcons()->iconPixmap(m_data.iconNumber); - } else { - Q_ASSERT(database()); - - return database()->metadata()->customIconScaledPixmap(m_data.customIcon); } + Q_ASSERT(database()); + return database()->metadata()->customIconScaledPixmap(m_data.customIcon); } int Entry::iconNumber() const @@ -195,7 +202,7 @@ QString Entry::tags() const return m_data.tags; } -TimeInfo Entry::timeInfo() const +const TimeInfo& Entry::timeInfo() const { return m_data.timeInfo; } @@ -300,7 +307,7 @@ QString Entry::notes() const bool Entry::isExpired() const { - return m_data.timeInfo.expires() && m_data.timeInfo.expiryTime() < QDateTime::currentDateTimeUtc(); + return m_data.timeInfo.expires() && m_data.timeInfo.expiryTime() < Clock::currentDateTimeUtc(); } bool Entry::hasReferences() const @@ -532,7 +539,7 @@ void Entry::removeHistoryItems(const QList& historyEntries) for (Entry* entry : historyEntries) { Q_ASSERT(!entry->parent()); - Q_ASSERT(entry->uuid() == uuid()); + Q_ASSERT(entry->uuid().isNull() || entry->uuid() == uuid()); Q_ASSERT(m_history.contains(entry)); m_history.removeOne(entry); @@ -597,6 +604,42 @@ void Entry::truncateHistory() } } +bool Entry::equals(const Entry* other, CompareItemOptions options) const +{ + if (!other) { + return false; + } + if (m_uuid != other->uuid()) { + return false; + } + if (!m_data.equals(other->m_data, options)) { + return false; + } + if (*m_customData != *other->m_customData) { + return false; + } + if (*m_attributes != *other->m_attributes) { + return false; + } + if (*m_attachments != *other->m_attachments) { + return false; + } + if (*m_autoTypeAssociations != *other->m_autoTypeAssociations) { + return false; + } + if (!options.testFlag(CompareItemIgnoreHistory)) { + if (m_history.count() != other->m_history.count()) { + return false; + } + for (int i = 0; i < m_history.count(); ++i) { + if (!m_history[i]->equals(other->m_history[i], options)) { + return false; + } + } + } + return true; +} + Entry* Entry::clone(CloneFlags flags) const { Entry* entry = new Entry(); @@ -613,12 +656,12 @@ Entry* Entry::clone(CloneFlags flags) const if (flags & CloneUserAsRef) { // Build the username reference - QString username = "{REF:U@I:" + m_uuid.toRfc4122().toHex() + "}"; + QString username = "{REF:U@I:" + uuidToHex() + "}"; entry->m_attributes->set(EntryAttributes::UserNameKey, username.toUpper(), m_attributes->isProtected(EntryAttributes::UserNameKey)); } if (flags & ClonePassAsRef) { - QString password = "{REF:P@I:" + m_uuid.toRfc4122().toHex() + "}"; + QString password = "{REF:P@I:" + uuidToHex() + "}"; entry->m_attributes->set(EntryAttributes::PasswordKey, password.toUpper(), m_attributes->isProtected(EntryAttributes::PasswordKey)); } @@ -635,7 +678,7 @@ Entry* Entry::clone(CloneFlags flags) const entry->setUpdateTimeinfo(true); if (flags & CloneResetTimeInfo) { - QDateTime now = QDateTime::currentDateTimeUtc(); + QDateTime now = Clock::currentDateTimeUtc(); entry->m_data.timeInfo.setCreationTime(now); entry->m_data.timeInfo.setLastModificationTime(now); entry->m_data.timeInfo.setLastAccessTime(now); @@ -835,7 +878,7 @@ QString Entry::referenceFieldValue(EntryReferenceType referenceType) const case EntryReferenceType::Notes: return notes(); case EntryReferenceType::QUuid: - return uuid().toRfc4122().toHex(); + return uuidToHex(); default: break; } @@ -880,7 +923,7 @@ void Entry::setGroup(Group* group) QObject::setParent(group); if (m_updateTimeinfo) { - m_data.timeInfo.setLocationChanged(QDateTime::currentDateTimeUtc()); + m_data.timeInfo.setLocationChanged(Clock::currentDateTimeUtc()); } } @@ -893,9 +936,16 @@ const Database* Entry::database() const { if (m_group) { return m_group->database(); - } else { - return nullptr; } + return nullptr; +} + +Database* Entry::database() +{ + if (m_group) { + return m_group->database(); + } + return nullptr; } QString Entry::maskPasswordPlaceholders(const QString& str) const @@ -955,9 +1005,11 @@ Entry::PlaceholderType Entry::placeholderType(const QString& placeholder) const { if (!placeholder.startsWith(QLatin1Char('{')) || !placeholder.endsWith(QLatin1Char('}'))) { return PlaceholderType::NotPlaceholder; - } else if (placeholder.startsWith(QLatin1Literal("{S:"))) { + } + if (placeholder.startsWith(QLatin1Literal("{S:"))) { return PlaceholderType::CustomAttribute; - } else if (placeholder.startsWith(QLatin1Literal("{REF:"))) { + } + if (placeholder.startsWith(QLatin1Literal("{REF:"))) { return PlaceholderType::Reference; } @@ -1020,3 +1072,64 @@ QString Entry::resolveUrl(const QString& url) const // No valid http URL's found return QString(""); } + +bool EntryData::operator==(const EntryData& other) const +{ + return equals(other, CompareItemDefault); +} + +bool EntryData::operator!=(const EntryData& other) const +{ + return !(*this == other); +} + +bool EntryData::equals(const EntryData& other, CompareItemOptions options) const +{ + if (::compare(iconNumber, other.iconNumber, options) != 0) { + return false; + } + if (::compare(customIcon, other.customIcon, options) != 0) { + return false; + } + if (::compare(foregroundColor, other.foregroundColor, options) != 0) { + return false; + } + if (::compare(backgroundColor, other.backgroundColor, options) != 0) { + return false; + } + if (::compare(overrideUrl, other.overrideUrl, options) != 0) { + return false; + } + if (::compare(tags, other.tags, options) != 0) { + return false; + } + if (::compare(autoTypeEnabled, other.autoTypeEnabled, options) != 0) { + return false; + } + if (::compare(autoTypeObfuscation, other.autoTypeObfuscation, options) != 0) { + return false; + } + if (::compare(defaultAutoTypeSequence, other.defaultAutoTypeSequence, options) != 0) { + return false; + } + if (!timeInfo.equals(other.timeInfo, options)) { + return false; + } + if (!totpSettings.isNull() && !other.totpSettings.isNull()) { + // Both have TOTP settings, compare them + if (::compare(totpSettings->key, other.totpSettings->key, options) != 0) { + return false; + } + if (::compare(totpSettings->digits, other.totpSettings->digits, options) != 0) { + return false; + } + if (::compare(totpSettings->step, other.totpSettings->step, options) != 0) { + return false; + } + } else if (totpSettings.isNull() != other.totpSettings.isNull()) { + // The existance of TOTP has changed between these entries + return false; + } + + return true; +} diff --git a/src/core/Entry.h b/src/core/Entry.h index aa2426c5e..05ed30bc0 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -65,6 +65,10 @@ struct EntryData QString defaultAutoTypeSequence; TimeInfo timeInfo; QSharedPointer totpSettings; + + bool operator==(const EntryData& other) const; + bool operator!=(const EntryData& other) const; + bool equals(const EntryData& other, CompareItemOptions options) const; }; class Entry : public QObject @@ -75,6 +79,7 @@ public: Entry(); ~Entry(); const QUuid& uuid() const; + const QString uuidToHex() const; QImage icon() const; QPixmap iconPixmap() const; QPixmap iconScaledPixmap() const; @@ -84,7 +89,7 @@ public: QColor backgroundColor() const; QString overrideUrl() const; QString tags() const; - TimeInfo timeInfo() const; + const TimeInfo& timeInfo() const; bool autoTypeEnabled() const; int autoTypeObfuscation() const; QString defaultAutoTypeSequence() const; @@ -143,6 +148,8 @@ public: void removeHistoryItems(const QList& historyEntries); void truncateHistory(); + bool equals(const Entry* other, CompareItemOptions options = CompareItemDefault) const; + enum CloneFlag { CloneNoFlags = 0, @@ -204,7 +211,10 @@ public: Group* group(); const Group* group() const; void setGroup(Group* group); + const Database* database() const; + Database* database(); + bool canUpdateTimeinfo() const; void setUpdateTimeinfo(bool value); signals: @@ -229,7 +239,6 @@ private: static EntryReferenceType referenceType(const QString& referenceStr); - const Database* database() const; template bool set(T& property, const T& value); QUuid m_uuid; @@ -238,8 +247,8 @@ private: QPointer m_attachments; QPointer m_autoTypeAssociations; QPointer m_customData; + QList m_history; // Items sorted from oldest to newest - QList m_history; Entry* m_tmpHistoryItem; bool m_modifiedSinceBegin; QPointer m_group; diff --git a/src/core/Group.cpp b/src/core/Group.cpp index f5338533b..4ff6e5b68 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -18,6 +18,7 @@ #include "Group.h" +#include "core/Clock.h" #include "core/Config.h" #include "core/DatabaseIcons.h" #include "core/Global.h" @@ -40,7 +41,7 @@ Group::Group() m_data.isExpanded = true; m_data.autoTypeEnabled = Inherit; m_data.searchingEnabled = Inherit; - m_data.mergeMode = ModeInherit; + m_data.mergeMode = Default; connect(m_customData, SIGNAL(modified()), this, SIGNAL(modified())); connect(this, SIGNAL(modified()), SLOT(updateTimeinfo())); @@ -48,6 +49,7 @@ Group::Group() Group::~Group() { + setUpdateTimeinfo(false); // Destroy entries and children manually so DeletedObjects can be added // to database. const QList entries = m_entries; @@ -62,7 +64,7 @@ Group::~Group() if (m_db && m_parent) { DeletedObject delGroup; - delGroup.deletionTime = QDateTime::currentDateTimeUtc(); + delGroup.deletionTime = Clock::currentDateTimeUtc(); delGroup.uuid = m_uuid; m_db->addDeletedObject(delGroup); } @@ -92,11 +94,16 @@ template inline bool Group::set(P& property, const V& value) } } +bool Group::canUpdateTimeinfo() const +{ + return m_updateTimeinfo; +} + void Group::updateTimeinfo() { if (m_updateTimeinfo) { - m_data.timeInfo.setLastModificationTime(QDateTime::currentDateTimeUtc()); - m_data.timeInfo.setLastAccessTime(QDateTime::currentDateTimeUtc()); + m_data.timeInfo.setLastModificationTime(Clock::currentDateTimeUtc()); + m_data.timeInfo.setLastAccessTime(Clock::currentDateTimeUtc()); } } @@ -110,6 +117,11 @@ const QUuid& Group::uuid() const return m_uuid; } +const QString Group::uuidToHex() const +{ + return QString::fromLatin1(m_uuid.toRfc4122().toHex()); +} + QString Group::name() const { return m_data.name; @@ -176,7 +188,7 @@ const QUuid& Group::iconUuid() const return m_data.customIcon; } -TimeInfo Group::timeInfo() const +const TimeInfo& Group::timeInfo() const { return m_data.timeInfo; } @@ -228,15 +240,13 @@ Group::TriState Group::searchingEnabled() const Group::MergeMode Group::mergeMode() const { - if (m_data.mergeMode == Group::MergeMode::ModeInherit) { + if (m_data.mergeMode == Group::MergeMode::Default) { if (m_parent) { return m_parent->mergeMode(); - } else { - return Group::MergeMode::KeepNewer; // fallback } - } else { - return m_data.mergeMode; + return Group::MergeMode::KeepNewer; // fallback } + return m_data.mergeMode; } Entry* Group::lastTopVisibleEntry() const @@ -246,7 +256,7 @@ Entry* Group::lastTopVisibleEntry() const bool Group::isExpired() const { - return m_data.timeInfo.expires() && m_data.timeInfo.expiryTime() < QDateTime::currentDateTimeUtc(); + return m_data.timeInfo.expires() && m_data.timeInfo.expiryTime() < Clock::currentDateTimeUtc(); } CustomData* Group::customData() @@ -259,6 +269,39 @@ const CustomData* Group::customData() const return m_customData; } +bool Group::equals(const Group* other, CompareItemOptions options) const +{ + if (!other) { + return false; + } + if (m_uuid != other->m_uuid) { + return false; + } + if (!m_data.equals(other->m_data, options)) { + return false; + } + if (m_customData != other->m_customData) { + return false; + } + if (m_children.count() != other->m_children.count()) { + return false; + } + if (m_entries.count() != other->m_entries.count()) { + return false; + } + for (int i = 0; i < m_children.count(); ++i) { + if (m_children[i]->uuid() != other->m_children[i]->uuid()) { + return false; + } + } + for (int i = 0; i < m_entries.count(); ++i) { + if (m_entries[i]->uuid() != other->m_entries[i]->uuid()) { + return false; + } + } + return true; +} + void Group::setUuid(const QUuid& uuid) { set(m_uuid, uuid); @@ -418,7 +461,7 @@ void Group::setParent(Group* parent, int index) } if (m_updateTimeinfo) { - m_data.timeInfo.setLocationChanged(QDateTime::currentDateTimeUtc()); + m_data.timeInfo.setLocationChanged(Clock::currentDateTimeUtc()); } emit modified(); @@ -536,7 +579,7 @@ Entry* Group::findEntry(QString entryId) return nullptr; } -Entry* Group::findEntryByUuid(const QUuid& uuid) +Entry* Group::findEntryByUuid(const QUuid& uuid) const { Q_ASSERT(!uuid.isNull()); for (Entry* entry : entriesRecursive(false)) { @@ -683,61 +726,7 @@ QSet Group::customIconsRecursive() const return result; } -void Group::merge(const Group* other) -{ - - Group* rootGroup = this; - while (rootGroup->parentGroup()) { - rootGroup = rootGroup->parentGroup(); - } - - // merge entries - const QList dbEntries = other->entries(); - for (Entry* entry : dbEntries) { - - Entry* existingEntry = rootGroup->findEntryByUuid(entry->uuid()); - - if (!existingEntry) { - // This entry does not exist at all. Create it. - qDebug("New entry %s detected. Creating it.", qPrintable(entry->title())); - entry->clone(Entry::CloneIncludeHistory)->setGroup(this); - } else { - // Entry is already present in the database. Update it. - 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())); - } - resolveEntryConflict(existingEntry, entry); - } - } - - // merge groups recursively - const QList dbChildren = other->children(); - for (Group* group : dbChildren) { - - Group* existingGroup = rootGroup->findChildByUuid(group->uuid()); - - if (!existingGroup) { - qDebug("New group %s detected. Creating it.", qPrintable(group->name())); - Group* newGroup = group->clone(Entry::CloneNoFlags, Group::CloneNoFlags); - newGroup->setParent(this); - newGroup->merge(group); - } else { - bool locationChanged = existingGroup->timeInfo().locationChanged() < group->timeInfo().locationChanged(); - if (locationChanged && existingGroup->parent() != this) { - existingGroup->setParent(this); - qDebug("Location changed for group %s. Updating it", qPrintable(existingGroup->name())); - } - resolveGroupConflict(existingGroup, group); - existingGroup->merge(group); - } - } - - emit modified(); -} - -Group* Group::findChildByUuid(const QUuid& uuid) +Group* Group::findGroupByUuid(const QUuid& uuid) { Q_ASSERT(!uuid.isNull()); for (Group* group : groupsRecursive(true)) { @@ -792,7 +781,7 @@ Group* Group::clone(Entry::CloneFlags entryFlags, Group::CloneFlags groupFlags) clonedGroup->setUpdateTimeinfo(true); if (groupFlags & Group::CloneResetTimeInfo) { - QDateTime now = QDateTime::currentDateTimeUtc(); + QDateTime now = Clock::currentDateTimeUtc(); clonedGroup->m_data.timeInfo.setCreationTime(now); clonedGroup->m_data.timeInfo.setLastModificationTime(now); clonedGroup->m_data.timeInfo.setLastAccessTime(now); @@ -828,7 +817,9 @@ void Group::addEntry(Entry* entry) void Group::removeEntry(Entry* entry) { - Q_ASSERT(m_entries.contains(entry)); + Q_ASSERT_X(m_entries.contains(entry), + Q_FUNC_INFO, + QString("Group %1 does not contain %2").arg(this->name()).arg(entry->title()).toLatin1()); emit entryAboutToRemove(entry); @@ -905,12 +896,6 @@ void Group::recCreateDelObjects() } } -void Group::markOlderEntry(Entry* entry) -{ - entry->attributes()->set( - "merged", tr("older entry merged from database \"%1\"").arg(entry->group()->database()->metadata()->name())); -} - bool Group::resolveSearchingEnabled() const { switch (m_data.searchingEnabled) { @@ -949,63 +934,6 @@ bool Group::resolveAutoTypeEnabled() const } } -void Group::resolveEntryConflict(Entry* existingEntry, Entry* otherEntry) -{ - const QDateTime timeExisting = existingEntry->timeInfo().lastModificationTime(); - const QDateTime timeOther = otherEntry->timeInfo().lastModificationTime(); - - Entry* clonedEntry; - - switch (mergeMode()) { - case KeepBoth: - // if one entry is newer, create a clone and add it to the group - if (timeExisting > timeOther) { - clonedEntry = otherEntry->clone(Entry::CloneNewUuid | Entry::CloneIncludeHistory); - clonedEntry->setGroup(this); - markOlderEntry(clonedEntry); - } else if (timeExisting < timeOther) { - clonedEntry = otherEntry->clone(Entry::CloneNewUuid | Entry::CloneIncludeHistory); - clonedEntry->setGroup(this); - markOlderEntry(existingEntry); - } - break; - case KeepNewer: - if (timeExisting < timeOther) { - qDebug("Updating entry %s.", qPrintable(existingEntry->title())); - // only if other entry is newer, replace existing one - Group* currentGroup = existingEntry->group(); - currentGroup->removeEntry(existingEntry); - otherEntry->clone(Entry::CloneIncludeHistory)->setGroup(currentGroup); - } - - break; - case KeepExisting: - break; - default: - // do nothing - break; - } -} - -void Group::resolveGroupConflict(Group* existingGroup, Group* otherGroup) -{ - const QDateTime timeExisting = existingGroup->timeInfo().lastModificationTime(); - const QDateTime timeOther = otherGroup->timeInfo().lastModificationTime(); - - // only if the other group is newer, update the existing one. - if (timeExisting < timeOther) { - qDebug("Updating group %s.", qPrintable(existingGroup->name())); - existingGroup->setName(otherGroup->name()); - existingGroup->setNotes(otherGroup->notes()); - if (otherGroup->iconNumber() == 0) { - existingGroup->setIcon(otherGroup->iconUuid()); - } else { - existingGroup->setIcon(otherGroup->iconNumber()); - } - existingGroup->setExpiryTime(otherGroup->timeInfo().expiryTime()); - } -} - QStringList Group::locate(QString locateTerm, QString currentPath) { Q_ASSERT(!locateTerm.isNull()); @@ -1054,3 +982,49 @@ Entry* Group::addEntryWithPath(QString entryPath) return entry; } + +bool Group::GroupData::operator==(const Group::GroupData& other) const +{ + return equals(other, CompareItemDefault); +} + +bool Group::GroupData::operator!=(const Group::GroupData& other) const +{ + return !(*this == other); +} + +bool Group::GroupData::equals(const Group::GroupData& other, CompareItemOptions options) const +{ + if (::compare(name, other.name, options) != 0) { + return false; + } + if (::compare(notes, other.notes, options) != 0) { + return false; + } + if (::compare(iconNumber, other.iconNumber) != 0) { + return false; + } + if (::compare(customIcon, other.customIcon) != 0) { + return false; + } + if (timeInfo.equals(other.timeInfo, options) != 0) { + return false; + } + // TODO HNH: Some properties are configurable - should they be ignored? + if (::compare(isExpanded, other.isExpanded, options) != 0) { + return false; + } + if (::compare(defaultAutoTypeSequence, other.defaultAutoTypeSequence, options) != 0) { + return false; + } + if (::compare(autoTypeEnabled, other.autoTypeEnabled, options) != 0) { + return false; + } + if (::compare(searchingEnabled, other.searchingEnabled, options) != 0) { + return false; + } + if (::compare(mergeMode, other.mergeMode, options) != 0) { + return false; + } + return true; +} diff --git a/src/core/Group.h b/src/core/Group.h index 35619d938..89343e829 100644 --- a/src/core/Group.h +++ b/src/core/Group.h @@ -42,10 +42,12 @@ public: }; enum MergeMode { - ModeInherit, - KeepBoth, - KeepNewer, - KeepExisting + Default, // Determine merge strategy from parent or fallback (Synchronize) + Duplicate, // lossy strategy regarding deletions, duplicate older changes in a new entry + KeepLocal, // merge history forcing local as top regardless of age + KeepRemote, // merge history forcing remote as top regardless of age + KeepNewer, // merge history + Synchronize, // merge history keeping most recent as top entry and appling deletions }; enum CloneFlag @@ -69,6 +71,10 @@ public: Group::TriState autoTypeEnabled; Group::TriState searchingEnabled; Group::MergeMode mergeMode; + + bool operator==(const GroupData& other) const; + bool operator!=(const GroupData& other) const; + bool equals(const GroupData& other, CompareItemOptions options) const; }; Group(); @@ -77,6 +83,7 @@ public: static Group* createRecycleBin(); const QUuid& uuid() const; + const QString uuidToHex() const; QString name() const; QString notes() const; QImage icon() const; @@ -84,7 +91,7 @@ public: QPixmap iconScaledPixmap() const; int iconNumber() const; const QUuid& iconUuid() const; - TimeInfo timeInfo() const; + const TimeInfo& timeInfo() const; bool isExpanded() const; QString defaultAutoTypeSequence() const; QString effectiveAutoTypeSequence() const; @@ -98,6 +105,8 @@ public: CustomData* customData(); const CustomData* customData() const; + bool equals(const Group* other, CompareItemOptions options) const; + static const int DefaultIconNumber; static const int RecycleBinIconNumber; static CloneFlags DefaultCloneFlags; @@ -105,10 +114,10 @@ public: static const QString RootAutoTypeSequence; Group* findChildByName(const QString& name); - Group* findChildByUuid(const QUuid& uuid); Entry* findEntry(QString entryId); - Entry* findEntryByUuid(const QUuid& uuid); + Entry* findEntryByUuid(const QUuid& uuid) const; Entry* findEntryByPath(QString entryPath, QString basePath = QString("")); + Group* findGroupByUuid(const QUuid& uuid); Group* findGroupByPath(QString groupPath); QStringList locate(QString locateTerm, QString currentPath = QString("/")); Entry* addEntryWithPath(QString entryPath); @@ -127,6 +136,7 @@ public: void setExpiryTime(const QDateTime& dateTime); void setMergeMode(MergeMode newMode); + bool canUpdateTimeinfo() const; void setUpdateTimeinfo(bool value); Group* parentGroup(); @@ -153,9 +163,10 @@ public: CloneFlags groupFlags = DefaultCloneFlags) const; void copyDataFrom(const Group* other); - void merge(const Group* other); QString print(bool recursive = false, int depth = 0); + void addEntry(Entry* entry); + void removeEntry(Entry* entry); signals: void dataChanged(Group* group); @@ -184,12 +195,7 @@ private slots: private: template bool set(P& property, const V& value); - void addEntry(Entry* entry); - void removeEntry(Entry* entry); void setParent(Database* db); - void markOlderEntry(Entry* entry); - void resolveEntryConflict(Entry* existingEntry, Entry* otherEntry); - void resolveGroupConflict(Group* existingGroup, Group* otherGroup); void recSetDatabase(Database* db); void cleanupParent(); diff --git a/src/core/Merger.cpp b/src/core/Merger.cpp new file mode 100644 index 000000000..9b87a6ac3 --- /dev/null +++ b/src/core/Merger.cpp @@ -0,0 +1,613 @@ +/* + * Copyright (C) 2018 KeePassXC Team + * + * 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 . + */ + +#include "Merger.h" + +#include "core/Clock.h" +#include "core/Database.h" +#include "core/Entry.h" +#include "core/Metadata.h" + +Merger::Merger(const Database* sourceDb, Database* targetDb) + : m_mode(Group::Default) +{ + if (!sourceDb || !targetDb) { + Q_ASSERT(sourceDb && targetDb); + return; + } + + m_context = MergeContext{ + sourceDb, targetDb, sourceDb->rootGroup(), targetDb->rootGroup(), sourceDb->rootGroup(), targetDb->rootGroup()}; +} + +Merger::Merger(const Group* sourceGroup, Group* targetGroup) + : m_mode(Group::Default) +{ + if (!sourceGroup || !targetGroup) { + Q_ASSERT(sourceGroup && targetGroup); + return; + } + + m_context = MergeContext{sourceGroup->database(), + targetGroup->database(), + sourceGroup->database()->rootGroup(), + targetGroup->database()->rootGroup(), + sourceGroup, + targetGroup}; +} + +void Merger::setForcedMergeMode(Group::MergeMode mode) +{ + m_mode = mode; +} + +void Merger::resetForcedMergeMode() +{ + m_mode = Group::Default; +} + +bool Merger::merge() +{ + // Order of merge steps is important - it is possible that we + // create some items before deleting them afterwards + ChangeList changes; + changes << mergeGroup(m_context); + changes << mergeDeletions(m_context); + changes << mergeMetadata(m_context); + + // qDebug("Merged %s", qPrintable(changes.join("\n\t"))); + + // At this point we have a list of changes we may want to show the user + if (!changes.isEmpty()) { + m_context.m_targetDb->markAsModified(); + return true; + } + return false; +} + +Merger::ChangeList Merger::mergeGroup(const MergeContext& context) +{ + ChangeList changes; + // merge entries + const QList sourceEntries = context.m_sourceGroup->entries(); + for (Entry* sourceEntry : sourceEntries) { + Entry* targetEntry = context.m_targetRootGroup->findEntryByUuid(sourceEntry->uuid()); + if (!targetEntry) { + changes << tr("Creating missing %1 [%2]").arg(sourceEntry->title(), sourceEntry->uuidToHex()); + // This entry does not exist at all. Create it. + targetEntry = sourceEntry->clone(Entry::CloneIncludeHistory); + moveEntry(targetEntry, context.m_targetGroup); + } else { + // Entry is already present in the database. Update it. + const bool locationChanged = targetEntry->timeInfo().locationChanged() < sourceEntry->timeInfo().locationChanged(); + if (locationChanged && targetEntry->group() != context.m_targetGroup) { + changes << tr("Relocating %1 [%2]").arg(sourceEntry->title()).arg(sourceEntry->uuidToHex()); + moveEntry(targetEntry, context.m_targetGroup); + } + changes << resolveEntryConflict(context, sourceEntry, targetEntry); + } + } + + // merge groups recursively + const QList sourceChildGroups = context.m_sourceGroup->children(); + for (Group* sourceChildGroup : sourceChildGroups) { + Group* targetChildGroup = context.m_targetRootGroup->findGroupByUuid(sourceChildGroup->uuid()); + if (!targetChildGroup) { + changes << tr("Creating missing %1 [%2]").arg(sourceChildGroup->name()).arg(sourceChildGroup->uuidToHex()); + targetChildGroup = sourceChildGroup->clone(Entry::CloneNoFlags, Group::CloneNoFlags); + moveGroup(targetChildGroup, context.m_targetGroup); + TimeInfo timeinfo = targetChildGroup->timeInfo(); + timeinfo.setLocationChanged(sourceChildGroup->timeInfo().locationChanged()); + targetChildGroup->setTimeInfo(timeinfo); + } else { + bool locationChanged = + targetChildGroup->timeInfo().locationChanged() < sourceChildGroup->timeInfo().locationChanged(); + if (locationChanged && targetChildGroup->parent() != context.m_targetGroup) { + changes << tr("Relocating %1 [%2]").arg(sourceChildGroup->name()).arg(sourceChildGroup->uuidToHex()); + moveGroup(targetChildGroup, context.m_targetGroup); + TimeInfo timeinfo = targetChildGroup->timeInfo(); + timeinfo.setLocationChanged(sourceChildGroup->timeInfo().locationChanged()); + targetChildGroup->setTimeInfo(timeinfo); + } + changes << resolveGroupConflict(context, sourceChildGroup, targetChildGroup); + } + MergeContext subcontext{context.m_sourceDb, + context.m_targetDb, + context.m_sourceRootGroup, + context.m_targetRootGroup, + sourceChildGroup, + targetChildGroup}; + changes << mergeGroup(subcontext); + } + return changes; +} + +Merger::ChangeList Merger::resolveGroupConflict(const MergeContext& context, const Group* sourceChildGroup, Group* targetChildGroup) +{ + Q_UNUSED(context); + ChangeList changes; + + const QDateTime timeExisting = targetChildGroup->timeInfo().lastModificationTime(); + const QDateTime timeOther = sourceChildGroup->timeInfo().lastModificationTime(); + + // only if the other group is newer, update the existing one. + if (timeExisting < timeOther) { + changes << tr("Overwriting %1 [%2]").arg(sourceChildGroup->name()).arg(sourceChildGroup->uuidToHex()); + targetChildGroup->setName(sourceChildGroup->name()); + targetChildGroup->setNotes(sourceChildGroup->notes()); + if (sourceChildGroup->iconNumber() == 0) { + targetChildGroup->setIcon(sourceChildGroup->iconUuid()); + } else { + targetChildGroup->setIcon(sourceChildGroup->iconNumber()); + } + targetChildGroup->setExpiryTime(sourceChildGroup->timeInfo().expiryTime()); + TimeInfo timeInfo = targetChildGroup->timeInfo(); + timeInfo.setLastModificationTime(timeOther); + targetChildGroup->setTimeInfo(timeInfo); + } + return changes; +} + +bool Merger::markOlderEntry(Entry* entry) +{ + entry->attributes()->set( + "merged", tr("older entry merged from database \"%1\"").arg(entry->group()->database()->metadata()->name())); + return true; +} + +void Merger::moveEntry(Entry* entry, Group* targetGroup) +{ + Q_ASSERT(entry); + Group* sourceGroup = entry->group(); + if (sourceGroup == targetGroup) { + return; + } + const bool sourceGroupUpdateTimeInfo = sourceGroup ? sourceGroup->canUpdateTimeinfo() : false; + if (sourceGroup) { + sourceGroup->setUpdateTimeinfo(false); + } + const bool targetGroupUpdateTimeInfo = targetGroup ? targetGroup->canUpdateTimeinfo() : false; + if (targetGroup) { + targetGroup->setUpdateTimeinfo(false); + } + const bool entryUpdateTimeInfo = entry->canUpdateTimeinfo(); + entry->setUpdateTimeinfo(false); + + entry->setGroup(targetGroup); + + entry->setUpdateTimeinfo(entryUpdateTimeInfo); + if (targetGroup) { + targetGroup->setUpdateTimeinfo(targetGroupUpdateTimeInfo); + } + if (sourceGroup) { + sourceGroup->setUpdateTimeinfo(sourceGroupUpdateTimeInfo); + } +} + +void Merger::moveGroup(Group* group, Group* targetGroup) +{ + Q_ASSERT(group); + Group* sourceGroup = group->parentGroup(); + if (sourceGroup == targetGroup) { + return; + } + const bool sourceGroupUpdateTimeInfo = sourceGroup ? sourceGroup->canUpdateTimeinfo() : false; + if (sourceGroup) { + sourceGroup->setUpdateTimeinfo(false); + } + const bool targetGroupUpdateTimeInfo = targetGroup ? targetGroup->canUpdateTimeinfo() : false; + if (targetGroup) { + targetGroup->setUpdateTimeinfo(false); + } + const bool groupUpdateTimeInfo = group->canUpdateTimeinfo(); + group->setUpdateTimeinfo(false); + + group->setParent(targetGroup); + + group->setUpdateTimeinfo(groupUpdateTimeInfo); + if (targetGroup) { + targetGroup->setUpdateTimeinfo(targetGroupUpdateTimeInfo); + } + if (sourceGroup) { + sourceGroup->setUpdateTimeinfo(sourceGroupUpdateTimeInfo); + } +} + +void Merger::eraseEntry(Entry* entry) +{ + Database* database = entry->database(); + // most simple method to remove an item from DeletedObjects :( + const QList deletions = database->deletedObjects(); + Group* parentGroup = entry->group(); + const bool groupUpdateTimeInfo = parentGroup ? parentGroup->canUpdateTimeinfo() : false; + if (parentGroup) { + parentGroup->setUpdateTimeinfo(false); + } + delete entry; + if (parentGroup) { + parentGroup->setUpdateTimeinfo(groupUpdateTimeInfo); + } + database->setDeletedObjects(deletions); +} + +void Merger::eraseGroup(Group* group) +{ + Database* database = group->database(); + // most simple method to remove an item from DeletedObjects :( + const QList deletions = database->deletedObjects(); + Group* parentGroup = group->parentGroup(); + const bool groupUpdateTimeInfo = parentGroup ? parentGroup->canUpdateTimeinfo() : false; + if (parentGroup) { + parentGroup->setUpdateTimeinfo(false); + } + delete group; + if (parentGroup) { + parentGroup->setUpdateTimeinfo(groupUpdateTimeInfo); + } + database->setDeletedObjects(deletions); +} + +Merger::ChangeList Merger::resolveEntryConflict_Duplicate(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry) +{ + ChangeList changes; + const int comparison = compare(targetEntry->timeInfo().lastModificationTime(), sourceEntry->timeInfo().lastModificationTime(), CompareItemIgnoreMilliseconds); + // if one entry is newer, create a clone and add it to the group + if (comparison < 0) { + Entry* clonedEntry = sourceEntry->clone(Entry::CloneNewUuid | Entry::CloneIncludeHistory); + moveEntry(clonedEntry, context.m_targetGroup); + markOlderEntry(targetEntry); + changes << tr("Adding backup for older target %1 [%2]") + .arg(targetEntry->title()) + .arg(targetEntry->uuidToHex()); + } else if (comparison > 0) { + Entry* clonedEntry = sourceEntry->clone(Entry::CloneNewUuid | Entry::CloneIncludeHistory); + moveEntry(clonedEntry, context.m_targetGroup); + markOlderEntry(clonedEntry); + changes << tr("Adding backup for older source %1 [%2]") + .arg(sourceEntry->title()) + .arg(sourceEntry->uuidToHex()); + } + return changes; +} + +Merger::ChangeList Merger::resolveEntryConflict_KeepLocal(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry) +{ + Q_UNUSED(context); + ChangeList changes; + const int comparison = compare(targetEntry->timeInfo().lastModificationTime(), sourceEntry->timeInfo().lastModificationTime(), CompareItemIgnoreMilliseconds); + if (comparison < 0) { + // we need to make our older entry "newer" than the new entry - therefore + // we just create a new history entry without any changes - this preserves + // the old state before merging the new state and updates the timestamp + // the merge takes care, that the newer entry is sorted inbetween both entries + // this type of merge changes the database timestamp since reapplying the + // old entry is an active change of the database! + changes << tr("Reapplying older target entry on top of newer source %1 [%2]") + .arg(targetEntry->title()) + .arg(targetEntry->uuidToHex()); + Entry* agedTargetEntry = targetEntry->clone(Entry::CloneNoFlags); + targetEntry->addHistoryItem(agedTargetEntry); + } + return changes; +} + +Merger::ChangeList Merger::resolveEntryConflict_KeepRemote(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry) +{ + Q_UNUSED(context); + ChangeList changes; + const int comparison = compare(targetEntry->timeInfo().lastModificationTime(), sourceEntry->timeInfo().lastModificationTime(), CompareItemIgnoreMilliseconds); + if (comparison > 0) { + // we need to make our older entry "newer" than the new entry - therefore + // we just create a new history entry without any changes - this preserves + // the old state before merging the new state and updates the timestamp + // the merge takes care, that the newer entry is sorted inbetween both entries + // this type of merge changes the database timestamp since reapplying the + // old entry is an active change of the database! + changes << tr("Reapplying older source entry on top of newer target %1 [%2]") + .arg(targetEntry->title()) + .arg(targetEntry->uuidToHex()); + targetEntry->beginUpdate(); + targetEntry->copyDataFrom(sourceEntry); + targetEntry->endUpdate(); + // History item is created by endUpdate since we should have changes + } + return changes; +} + + +Merger::ChangeList Merger::resolveEntryConflict_MergeHistories(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry, Group::MergeMode mergeMethod) +{ + Q_UNUSED(context); + + ChangeList changes; + const int comparison = compare(targetEntry->timeInfo().lastModificationTime(), sourceEntry->timeInfo().lastModificationTime(), CompareItemIgnoreMilliseconds); + if (comparison < 0) { + Group* currentGroup = targetEntry->group(); + Entry* clonedEntry = sourceEntry->clone(Entry::CloneIncludeHistory); + qDebug("Merge %s/%s with alien on top under %s", + qPrintable(targetEntry->title()), + qPrintable(sourceEntry->title()), + qPrintable(currentGroup->name())); + changes << tr("Synchronizing from newer source %1 [%2]") + .arg(targetEntry->title()) + .arg(targetEntry->uuidToHex()); + moveEntry(clonedEntry, currentGroup); + mergeHistory(targetEntry, clonedEntry, mergeMethod); + eraseEntry(targetEntry); + } else { + qDebug("Merge %s/%s with local on top/under %s", + qPrintable(targetEntry->title()), + qPrintable(sourceEntry->title()), + qPrintable(targetEntry->group()->name())); + const bool changed = mergeHistory(sourceEntry, targetEntry, mergeMethod); + if (changed) { + changes << tr("Synchronizing from older source %1 [%2]") + .arg(targetEntry->title()) + .arg(targetEntry->uuidToHex()); + } + } + return changes; +} + + +Merger::ChangeList Merger::resolveEntryConflict(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry) +{ + ChangeList changes; + // We need to cut off the milliseconds since the persistent format only supports times down to seconds + // so when we import data from a remote source, it may represent the (or even some msec newer) data + // which may be discarded due to higher runtime precision + + Group::MergeMode mergeMode = m_mode == Group::Default ? context.m_targetGroup->mergeMode() : m_mode; + switch (mergeMode) { + case Group::Duplicate: + changes << resolveEntryConflict_Duplicate(context, sourceEntry, targetEntry); + break; + + case Group::KeepLocal: + changes << resolveEntryConflict_KeepLocal(context, sourceEntry, targetEntry); + changes << resolveEntryConflict_MergeHistories(context, sourceEntry, targetEntry, mergeMode); + break; + + case Group::KeepRemote: + changes << resolveEntryConflict_KeepRemote(context, sourceEntry, targetEntry); + changes << resolveEntryConflict_MergeHistories(context, sourceEntry, targetEntry, mergeMode); + break; + + case Group::Synchronize: + case Group::KeepNewer: + // nothing special to do since resolveEntryConflictMergeHistories takes care to use the newest entry + changes << resolveEntryConflict_MergeHistories(context, sourceEntry, targetEntry, mergeMode); + break; + + default: + // do nothing + break; + } + return changes; +} + +bool Merger::mergeHistory(const Entry* sourceEntry, Entry* targetEntry, Group::MergeMode mergeMethod) +{ + Q_UNUSED(mergeMethod); + const auto targetHistoryItems = targetEntry->historyItems(); + const auto sourceHistoryItems = sourceEntry->historyItems(); + const int comparison = compare(sourceEntry->timeInfo().lastModificationTime(), targetEntry->timeInfo().lastModificationTime(), CompareItemIgnoreMilliseconds); + const bool preferLocal = mergeMethod == Group::KeepLocal || comparison < 0; + const bool preferRemote = mergeMethod == Group::KeepRemote || comparison > 0; + + QMap merged; + for (Entry* historyItem : targetHistoryItems) { + const QDateTime modificationTime = Clock::serialized(historyItem->timeInfo().lastModificationTime()); + if (merged.contains(modificationTime) && !merged[modificationTime]->equals(historyItem, CompareItemIgnoreMilliseconds)) { + ::qWarning("Inconsistent history entry of %s[%s] at %s contains conflicting changes - conflict resolution may lose data!", + qPrintable(sourceEntry->title()), + qPrintable(sourceEntry->uuidToHex()), + qPrintable(modificationTime.toString("yyyy-MM-dd HH-mm-ss-zzz"))); + } + merged[modificationTime] = historyItem->clone(Entry::CloneNoFlags); + } + for (Entry* historyItem : sourceHistoryItems) { + // Items with same modification-time changes will be regarded as same (like KeePass2) + const QDateTime modificationTime = Clock::serialized(historyItem->timeInfo().lastModificationTime()); + if (merged.contains(modificationTime) && !merged[modificationTime]->equals(historyItem, CompareItemIgnoreMilliseconds)) { + ::qWarning("History entry of %s[%s] at %s contains conflicting changes - conflict resolution may lose data!", + qPrintable(sourceEntry->title()), + qPrintable(sourceEntry->uuidToHex()), + qPrintable(modificationTime.toString("yyyy-MM-dd HH-mm-ss-zzz"))); + } + if (preferRemote && merged.contains(modificationTime)) { + // forcefully apply the remote history item + delete merged.take(modificationTime); + } + if (!merged.contains(modificationTime)) { + merged[modificationTime] = historyItem->clone(Entry::CloneNoFlags); + } + } + + const QDateTime targetModificationTime = Clock::serialized(targetEntry->timeInfo().lastModificationTime()); + const QDateTime sourceModificationTime = Clock::serialized(sourceEntry->timeInfo().lastModificationTime()); + if (targetModificationTime == sourceModificationTime && !targetEntry->equals(sourceEntry, CompareItemIgnoreMilliseconds | CompareItemIgnoreHistory | CompareItemIgnoreLocation)) { + ::qWarning("Entry of %s[%s] contains conflicting changes - conflict resolution may lose data!", + qPrintable(sourceEntry->title()), + qPrintable(sourceEntry->uuidToHex())); + } + + if (targetModificationTime < sourceModificationTime) { + if (preferLocal && merged.contains(targetModificationTime)) { + // forcefully apply the local history item + delete merged.take(targetModificationTime); + } + if (!merged.contains(targetModificationTime)) { + merged[targetModificationTime] = targetEntry->clone(Entry::CloneNoFlags); + } + } else if (targetModificationTime > sourceModificationTime) { + if (preferRemote && !merged.contains(sourceModificationTime)) { + // forcefully apply the remote history item + delete merged.take(sourceModificationTime); + } + if (!merged.contains(sourceModificationTime)) { + merged[sourceModificationTime] = sourceEntry->clone(Entry::CloneNoFlags); + } + } + + bool changed = false; + const int maxItems = targetEntry->database()->metadata()->historyMaxItems(); + const auto updatedHistoryItems = merged.values(); + for (int i = 0; i < maxItems; ++i) { + const Entry* oldEntry = targetHistoryItems.value(targetHistoryItems.count() - i); + const Entry* newEntry = updatedHistoryItems.value(updatedHistoryItems.count() - i); + if (!oldEntry && !newEntry) { + continue; + } + if (oldEntry && newEntry && oldEntry->equals(newEntry, CompareItemIgnoreMilliseconds)) { + continue; + } + changed = true; + break; + } + if (!changed) { + qDeleteAll(updatedHistoryItems); + return false; + } + // We need to prevent any modification to the database since every change should be tracked either + // in a clone history item or in the Entry itself + const TimeInfo timeInfo = targetEntry->timeInfo(); + const bool blockedSignals = targetEntry->blockSignals(true); + bool updateTimeInfo = targetEntry->canUpdateTimeinfo(); + targetEntry->setUpdateTimeinfo(false); + targetEntry->removeHistoryItems(targetHistoryItems); + for (Entry* historyItem : merged.values()) { + Q_ASSERT(!historyItem->parent()); + targetEntry->addHistoryItem(historyItem); + } + targetEntry->truncateHistory(); + targetEntry->blockSignals(blockedSignals); + targetEntry->setUpdateTimeinfo(updateTimeInfo); + Q_ASSERT(timeInfo == targetEntry->timeInfo()); + Q_UNUSED(timeInfo); + return true; +} + +Merger::ChangeList Merger::mergeDeletions(const MergeContext& context) +{ + ChangeList changes; + Group::MergeMode mergeMode = m_mode == Group::Default ? context.m_targetGroup->mergeMode() : m_mode; + if (mergeMode != Group::Synchronize) { + // no deletions are applied for any other strategy! + return changes; + } + + const auto targetDeletions = context.m_targetDb->deletedObjects(); + const auto sourceDeletions = context.m_sourceDb->deletedObjects(); + + QList deletions; + QMap mergedDeletions; + QList entries; + QList groups; + + for (const auto& object : (targetDeletions + sourceDeletions)) { + if (!mergedDeletions.contains(object.uuid)) { + mergedDeletions[object.uuid] = object; + + auto* entry = context.m_targetRootGroup->findEntryByUuid(object.uuid); + if (entry) { + entries << entry; + continue; + } + auto* group = context.m_targetRootGroup->findGroupByUuid(object.uuid); + if (group) { + groups << group; + continue; + } + deletions << object; + continue; + } + if (mergedDeletions[object.uuid].deletionTime > object.deletionTime) { + mergedDeletions[object.uuid] = object; + } + } + + while (!entries.isEmpty()) { + auto* entry = entries.takeFirst(); + const auto& object = mergedDeletions[entry->uuid()]; + if (entry->timeInfo().lastModificationTime() > object.deletionTime) { + // keep deleted entry since it was changed after deletion date + continue; + } + deletions << object; + if (entry->group()) { + changes << tr("Deleting child %1 [%2]").arg(entry->title()).arg(entry->uuidToHex()); + } else { + changes << tr("Deleting orphan %1 [%2]").arg(entry->title()).arg(entry->uuidToHex()); + } + // Entry is inserted into deletedObjects after deletions are processed + eraseEntry(entry); + } + + while (!groups.isEmpty()) { + auto* group = groups.takeFirst(); + if (!(group->children().toSet() & groups.toSet()).isEmpty()) { + // we need to finish all children before we are able to determine if the group can be removed + groups << group; + continue; + } + const auto& object = mergedDeletions[group->uuid()]; + if (group->timeInfo().lastModificationTime() > object.deletionTime) { + // keep deleted group since it was changed after deletion date + continue; + } + if (!group->entriesRecursive(false).isEmpty() || !group->groupsRecursive(false).isEmpty()) { + // keep deleted group since it contains undeleted content + continue; + } + deletions << object; + if (group->parentGroup()) { + changes << tr("Deleting child %1 [%2]").arg(group->name()).arg(group->uuidToHex()); + } else { + changes << tr("Deleting orphan %1 [%2]").arg(group->name()).arg(group->uuidToHex()); + } + eraseGroup(group); + } + // Put every deletion to the earliest date of deletion + if (deletions != context.m_targetDb->deletedObjects()) { + changes << tr("Changed deleted objects"); + } + context.m_targetDb->setDeletedObjects(deletions); + return changes; +} + +Merger::ChangeList Merger::mergeMetadata(const MergeContext& context) +{ + // TODO HNH: missing handling of recycle bin, names, templates for groups and entries, + // public data (entries of newer dict override keys of older dict - ignoring + // their own age - it is enough if one entry of the whole dict is newer) => possible lost update + // TODO HNH: CustomData is merged with entries of the new customData overwrite entries + // of the older CustomData - the dict with the newest entry is considered + // newer regardless of the age of the other entries => possible lost update + ChangeList changes; + auto* sourceMetadata = context.m_sourceDb->metadata(); + auto* targetMetadata = context.m_targetDb->metadata(); + + for (QUuid customIconId : sourceMetadata->customIcons().keys()) { + QImage customIcon = sourceMetadata->customIcon(customIconId); + if (!targetMetadata->containsCustomIcon(customIconId)) { + targetMetadata->addCustomIcon(customIconId, customIcon); + changes << tr("Adding missing icon %1").arg(QString::fromLatin1(customIconId.toRfc4122().toHex())); + } + } + return changes; +} diff --git a/src/core/Merger.h b/src/core/Merger.h new file mode 100644 index 000000000..1f16fe026 --- /dev/null +++ b/src/core/Merger.h @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2018 KeePassXC Team + * + * 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 . + */ + +#ifndef KEEPASSXC_MERGER_H +#define KEEPASSXC_MERGER_H + +#include "core/Group.h" +#include +#include + +class Database; +class Entry; + +class Merger : public QObject +{ + Q_OBJECT +public: + Merger(const Database* sourceDb, Database* targetDb); + Merger(const Group* sourceGroup, Group* targetGroup); + void setForcedMergeMode(Group::MergeMode mode); + void resetForcedMergeMode(); + bool merge(); + +private: + typedef QString Change; + typedef QStringList ChangeList; + + struct MergeContext + { + QPointer m_sourceDb; + QPointer m_targetDb; + QPointer m_sourceRootGroup; + QPointer m_targetRootGroup; + QPointer m_sourceGroup; + QPointer m_targetGroup; + }; + ChangeList mergeGroup(const MergeContext& context); + ChangeList mergeDeletions(const MergeContext& context); + ChangeList mergeMetadata(const MergeContext& context); + bool markOlderEntry(Entry* entry); + bool mergeHistory(const Entry* sourceEntry, Entry* targetEntry, Group::MergeMode mergeMethod); + void moveEntry(Entry* entry, Group* targetGroup); + void moveGroup(Group* group, Group* targetGroup); + void eraseEntry(Entry* entry); // remove an entry without a trace in the deletedObjects - needed for elemination cloned entries + void eraseGroup(Group* group); // remove an entry without a trace in the deletedObjects - needed for elemination cloned entries + ChangeList resolveEntryConflict(const MergeContext& context, const Entry* existingEntry, Entry* otherEntry); + ChangeList resolveGroupConflict(const MergeContext& context, const Group* existingGroup, Group* otherGroup); + Merger::ChangeList resolveEntryConflict_Duplicate(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry); + Merger::ChangeList resolveEntryConflict_KeepLocal(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry); + Merger::ChangeList resolveEntryConflict_KeepRemote(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry); + Merger::ChangeList resolveEntryConflict_MergeHistories(const MergeContext& context, const Entry* sourceEntry, Entry* targetEntry, Group::MergeMode mergeMethod); + +private: + MergeContext m_context; + Group::MergeMode m_mode; +}; + +#endif // KEEPASSXC_MERGER_H diff --git a/src/core/Metadata.cpp b/src/core/Metadata.cpp index 54f99de22..ac9d38fda 100644 --- a/src/core/Metadata.cpp +++ b/src/core/Metadata.cpp @@ -18,6 +18,7 @@ #include "Metadata.h" #include +#include "core/Clock.h" #include "core/Entry.h" #include "core/Group.h" #include "core/Tools.h" @@ -43,7 +44,7 @@ Metadata::Metadata(QObject* parent) m_data.protectUrl = false; m_data.protectNotes = false; - QDateTime now = QDateTime::currentDateTimeUtc(); + QDateTime now = Clock::currentDateTimeUtc(); m_data.nameChanged = now; m_data.descriptionChanged = now; m_data.defaultUserNameChanged = now; @@ -71,7 +72,7 @@ template bool Metadata::set(P& property, const V& value, QDat if (property != value) { property = value; if (m_updateDatetime) { - dateTime = QDateTime::currentDateTimeUtc(); + dateTime = Clock::currentDateTimeUtc(); } emit modified(); return true; diff --git a/src/core/TimeInfo.cpp b/src/core/TimeInfo.cpp index 85c53a567..c774a7c81 100644 --- a/src/core/TimeInfo.cpp +++ b/src/core/TimeInfo.cpp @@ -17,11 +17,13 @@ #include "TimeInfo.h" +#include "core/Clock.h" + TimeInfo::TimeInfo() : m_expires(false) , m_usageCount(0) { - QDateTime now = QDateTime::currentDateTimeUtc(); + QDateTime now = Clock::currentDateTimeUtc(); m_lastModificationTime = now; m_creationTime = now; m_lastAccessTime = now; @@ -103,3 +105,38 @@ void TimeInfo::setLocationChanged(const QDateTime& dateTime) Q_ASSERT(dateTime.timeSpec() == Qt::UTC); m_locationChanged = dateTime; } + +bool TimeInfo::operator==(const TimeInfo& other) const +{ + return equals(other, CompareItemDefault); +} + +bool TimeInfo::operator!=(const TimeInfo& other) const +{ + return !this->operator==(other); +} + +bool TimeInfo::equals(const TimeInfo& other, CompareItemOptions options) const +{ + if (::compare(m_lastModificationTime, other.m_lastModificationTime, options) != 0) { + return false; + } + if (::compare(m_creationTime, other.m_creationTime, options) != 0) { + return false; + } + if (::compare(!options.testFlag(CompareItemIgnoreStatistics), m_lastAccessTime, other.m_lastAccessTime, options) + != 0) { + return false; + } + if (::compare(m_expires, m_expiryTime, other.m_expires, other.expiryTime(), options) != 0) { + return false; + } + if (::compare(!options.testFlag(CompareItemIgnoreStatistics), m_usageCount, other.m_usageCount, options) != 0) { + return false; + } + if (::compare(!options.testFlag(CompareItemIgnoreLocation), m_locationChanged, other.m_locationChanged, options) + != 0) { + return false; + } + return true; +} diff --git a/src/core/TimeInfo.h b/src/core/TimeInfo.h index 455c002cd..de8a37593 100644 --- a/src/core/TimeInfo.h +++ b/src/core/TimeInfo.h @@ -19,6 +19,9 @@ #define KEEPASSX_TIMEINFO_H #include +#include + +#include "core/Compare.h" class TimeInfo { @@ -33,6 +36,10 @@ public: int usageCount() const; QDateTime locationChanged() const; + bool operator==(const TimeInfo& other) const; + bool operator!=(const TimeInfo& other) const; + bool equals(const TimeInfo& other, CompareItemOptions options = CompareItemDefault) const; + void setLastModificationTime(const QDateTime& dateTime); void setCreationTime(const QDateTime& dateTime); void setLastAccessTime(const QDateTime& dateTime); diff --git a/src/core/Tools.h b/src/core/Tools.h index 9fd497995..4f75b750b 100644 --- a/src/core/Tools.h +++ b/src/core/Tools.h @@ -21,7 +21,6 @@ #include "core/Global.h" -#include #include #include diff --git a/src/format/KdbxXmlReader.cpp b/src/format/KdbxXmlReader.cpp index d9b6534bc..76fa03221 100644 --- a/src/format/KdbxXmlReader.cpp +++ b/src/format/KdbxXmlReader.cpp @@ -17,6 +17,7 @@ #include "KdbxXmlReader.h" #include "KeePass2RandomStream.h" +#include "core/Clock.h" #include "core/DatabaseIcons.h" #include "core/Endian.h" #include "core/Entry.h" @@ -1032,7 +1033,7 @@ QDateTime KdbxXmlReader::readDateTime() return QDateTime(QDate(1, 1, 1), QTime(0, 0, 0, 0), Qt::UTC).addSecs(secs); } - QDateTime dt = QDateTime::fromString(str, Qt::ISODate); + QDateTime dt = Clock::parse(str, Qt::ISODate); if (dt.isValid()) { return dt; } @@ -1041,7 +1042,7 @@ QDateTime KdbxXmlReader::readDateTime() raiseError(tr("Invalid date time value")); } - return QDateTime::currentDateTimeUtc(); + return Clock::currentDateTimeUtc(); } QColor KdbxXmlReader::readColor() diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 4453fb36c..aae6527a1 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -36,6 +36,7 @@ #include "core/EntrySearcher.h" #include "core/FilePath.h" #include "core/Group.h" +#include "core/Merger.h" #include "core/Metadata.h" #include "core/Tools.h" #include "format/KeePass2Reader.h" @@ -841,7 +842,8 @@ void DatabaseWidget::mergeDatabase(bool accepted) return; } - m_db->merge(srcDb); + Merger merger(srcDb, m_db); + merger.merge(); } m_databaseOpenMergeWidget->clearForms(); @@ -1244,7 +1246,7 @@ void DatabaseWidget::reloadDatabaseFile() if (mb == QMessageBox::No) { // Notify everyone the database does not match the file - emit m_db->modified(); + m_db->markAsModified(); m_databaseModified = true; // Rewatch the database file m_fileWatcher.addPath(m_filePath); @@ -1269,7 +1271,8 @@ void DatabaseWidget::reloadDatabaseFile() if (mb == QMessageBox::Yes) { // Merge the old database into the new one m_db->setEmitModified(false); - db->merge(m_db); + Merger merger(m_db, db); + merger.merge(); } else { // Since we are accepting the new file as-is, internally mark as unmodified // TODO: when saving is moved out of DatabaseTabWidget, this should be replaced @@ -1300,7 +1303,7 @@ void DatabaseWidget::reloadDatabaseFile() MessageWidget::Error); // HACK: Directly calling the database's signal // Mark db as modified since existing data may differ from file or file was deleted - m_db->modified(); + m_db->markAsModified(); } // Rewatch the database file diff --git a/src/gui/TotpDialog.cpp b/src/gui/TotpDialog.cpp index c2de9adbd..a9e4272d2 100644 --- a/src/gui/TotpDialog.cpp +++ b/src/gui/TotpDialog.cpp @@ -19,6 +19,7 @@ #include "TotpDialog.h" #include "ui_TotpDialog.h" +#include "core/Clock.h" #include "core/Config.h" #include "gui/Clipboard.h" @@ -77,7 +78,7 @@ void TotpDialog::updateProgressBar() void TotpDialog::updateSeconds() { - uint epoch = QDateTime::currentDateTime().toTime_t() - 1; + uint epoch = Clock::currentSecondsSinceEpoch() - 1; m_ui->timerLabel->setText(tr("Expires in %n second(s)", "", m_step - (epoch % m_step))); } @@ -91,6 +92,6 @@ void TotpDialog::updateTotp() void TotpDialog::resetCounter() { - uint epoch = QDateTime::currentDateTime().toTime_t(); + uint epoch = Clock::currentSecondsSinceEpoch(); m_counter = static_cast(static_cast(epoch % m_step) / m_step * 100); } diff --git a/src/gui/csvImport/CsvImportWidget.cpp b/src/gui/csvImport/CsvImportWidget.cpp index 45e0da247..662a9744e 100644 --- a/src/gui/csvImport/CsvImportWidget.cpp +++ b/src/gui/csvImport/CsvImportWidget.cpp @@ -23,6 +23,7 @@ #include #include +#include "core/Clock.h" #include "format/KeePass2Writer.h" #include "gui/MessageBox.h" #include "gui/MessageWidget.h" @@ -255,14 +256,13 @@ void CsvImportWidget::writeDatabase() if (m_parserModel->data(m_parserModel->index(r, 6)).isValid()) { qint64 lastModified = m_parserModel->data(m_parserModel->index(r, 6)).toString().toLongLong(); if (lastModified) { - timeInfo.setLastModificationTime( - QDateTime::fromMSecsSinceEpoch(lastModified * 1000).toTimeSpec(Qt::UTC)); + timeInfo.setLastModificationTime(Clock::datetimeUtc(lastModified * 1000)); } } if (m_parserModel->data(m_parserModel->index(r, 7)).isValid()) { qint64 created = m_parserModel->data(m_parserModel->index(r, 7)).toString().toLongLong(); if (created) { - timeInfo.setCreationTime(QDateTime::fromMSecsSinceEpoch(created * 1000).toTimeSpec(Qt::UTC)); + timeInfo.setCreationTime(Clock::datetimeUtc(created * 1000)); } } entry->setTimeInfo(timeInfo); diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetGeneral.cpp b/src/gui/dbsettings/DatabaseSettingsWidgetGeneral.cpp index 187dccc50..709e8c102 100644 --- a/src/gui/dbsettings/DatabaseSettingsWidgetGeneral.cpp +++ b/src/gui/dbsettings/DatabaseSettingsWidgetGeneral.cpp @@ -17,6 +17,7 @@ #include "DatabaseSettingsWidgetGeneral.h" #include "ui_DatabaseSettingsWidgetGeneral.h" +#include "core/Clock.h" #include "core/Database.h" #include "core/Entry.h" #include "core/Group.h" @@ -82,7 +83,7 @@ bool DatabaseSettingsWidgetGeneral::save() meta->setDescription(m_ui->dbDescriptionEdit->text()); meta->setDefaultUserName(m_ui->defaultUsernameEdit->text()); meta->setRecycleBinEnabled(m_ui->recycleBinEnabledCheckBox->isChecked()); - meta->setSettingsChanged(QDateTime::currentDateTimeUtc()); + meta->setSettingsChanged(Clock::currentDateTimeUtc()); bool truncate = false; diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index 83eb35613..f15ca5328 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -23,6 +23,7 @@ #include "ui_EditEntryWidgetMain.h" #include "ui_EditEntryWidgetSSHAgent.h" +#include #include #include #include @@ -32,9 +33,9 @@ #include #include #include -#include #include "autotype/AutoType.h" +#include "core/Clock.h" #include "core/Config.h" #include "core/Database.h" #include "core/Entry.h" @@ -619,7 +620,7 @@ void EditEntryWidget::useExpiryPreset(QAction* action) { m_mainUi->expireCheck->setChecked(true); TimeDelta delta = action->data().value(); - QDateTime now = QDateTime::currentDateTime(); + QDateTime now = Clock::currentDateTime(); QDateTime expiryDateTime = now + delta; m_mainUi->expireDatePicker->setDateTime(expiryDateTime); } diff --git a/src/gui/group/EditGroupWidget.h b/src/gui/group/EditGroupWidget.h index 8f13ef337..87271871d 100644 --- a/src/gui/group/EditGroupWidget.h +++ b/src/gui/group/EditGroupWidget.h @@ -60,11 +60,12 @@ private: Group::TriState triStateFromIndex(int index); const QScopedPointer m_mainUi; - QWidget* const m_editGroupWidgetMain; - EditWidgetIcons* const m_editGroupWidgetIcons; - EditWidgetProperties* const m_editWidgetProperties; - Group* m_group; - Database* m_database; + QPointer m_editGroupWidgetMain; + QPointer m_editGroupWidgetIcons; + QPointer m_editWidgetProperties; + + QPointer m_group; + QPointer m_database; Q_DISABLE_COPY(EditGroupWidget) }; diff --git a/src/totp/totp.cpp b/src/totp/totp.cpp index 8d924d579..efd83c8aa 100644 --- a/src/totp/totp.cpp +++ b/src/totp/totp.cpp @@ -18,9 +18,9 @@ #include "totp.h" #include "core/Base32.h" +#include "core/Clock.h" #include -#include #include #include #include @@ -133,8 +133,7 @@ QString Totp::generateTotp(const QSharedPointer settings, const quint64 current; if (time == 0) { - // TODO: Replace toTime_t() with toSecsSinceEpoch() when minimum Qt >= 5.8 - current = qToBigEndian(static_cast(QDateTime::currentDateTime().toTime_t()) / step); + current = qToBigEndian(static_cast(Clock::currentSecondsSinceEpoch()) / step); } else { current = qToBigEndian(time / step); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 261c9627e..73262bae0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -95,7 +95,7 @@ set(TEST_LIBRARIES ${ZLIB_LIBRARIES} ) -set(testsupport_SOURCES TestGlobal.h modeltest.cpp FailDevice.cpp) +set(testsupport_SOURCES TestGlobal.h modeltest.cpp FailDevice.cpp stub/TestClock.cpp) add_library(testsupport STATIC ${testsupport_SOURCES}) target_link_libraries(testsupport Qt5::Core Qt5::Concurrent Qt5::Widgets Qt5::Test) @@ -104,16 +104,16 @@ if(YUBIKEY_FOUND) endif() add_unit_test(NAME testgroup SOURCES TestGroup.cpp - LIBS ${TEST_LIBRARIES}) + LIBS testsupport ${TEST_LIBRARIES}) add_unit_test(NAME testkdbx2 SOURCES TestKdbx2.cpp LIBS ${TEST_LIBRARIES}) add_unit_test(NAME testkdbx3 SOURCES TestKeePass2Format.cpp FailDevice.cpp TestKdbx3.cpp - LIBS ${TEST_LIBRARIES}) + LIBS testsupport ${TEST_LIBRARIES}) add_unit_test(NAME testkdbx4 SOURCES TestKeePass2Format.cpp FailDevice.cpp mock/MockChallengeResponseKey.cpp TestKdbx4.cpp - LIBS ${TEST_LIBRARIES}) + LIBS testsupport ${TEST_LIBRARIES}) add_unit_test(NAME testkeys SOURCES TestKeys.cpp mock/MockChallengeResponseKey.cpp LIBS ${TEST_LIBRARIES}) @@ -137,7 +137,7 @@ add_unit_test(NAME testkeepass2randomstream SOURCES TestKeePass2RandomStream.cpp LIBS ${TEST_LIBRARIES}) add_unit_test(NAME testmodified SOURCES TestModified.cpp - LIBS ${TEST_LIBRARIES}) + LIBS testsupport ${TEST_LIBRARIES}) add_unit_test(NAME testdeletedobjects SOURCES TestDeletedObjects.cpp LIBS ${TEST_LIBRARIES}) @@ -163,7 +163,7 @@ add_unit_test(NAME testentry SOURCES TestEntry.cpp LIBS ${TEST_LIBRARIES}) add_unit_test(NAME testmerge SOURCES TestMerge.cpp - LIBS ${TEST_LIBRARIES}) + LIBS testsupport ${TEST_LIBRARIES}) add_unit_test(NAME testtotp SOURCES TestTotp.cpp LIBS ${TEST_LIBRARIES}) diff --git a/tests/TestEntry.cpp b/tests/TestEntry.cpp index 5c3cde618..8109f9bd9 100644 --- a/tests/TestEntry.cpp +++ b/tests/TestEntry.cpp @@ -20,6 +20,7 @@ #include "TestEntry.h" #include "TestGlobal.h" +#include "core/Clock.h" #include "crypto/Crypto.h" QTEST_GUILESS_MAIN(TestEntry) @@ -88,9 +89,7 @@ void TestEntry::testClone() entryOrg->setTitle("New Title"); entryOrg->endUpdate(); TimeInfo entryOrgTime = entryOrg->timeInfo(); - QDateTime dateTime; - dateTime.setTimeSpec(Qt::UTC); - dateTime.setTime_t(60); + QDateTime dateTime = Clock::datetimeUtc(60); entryOrgTime.setCreationTime(dateTime); entryOrg->setTimeInfo(entryOrgTime); @@ -225,7 +224,7 @@ void TestEntry::testResolveRecursivePlaceholders() entry2->setUuid(QUuid::createUuid()); entry2->setTitle("Entry2Title"); entry2->setUsername("{S:CustomUserNameAttribute}"); - entry2->setPassword(QString("{REF:P@I:%1}").arg(QString(entry1->uuid().toRfc4122().toHex()))); + entry2->setPassword(QString("{REF:P@I:%1}").arg(entry1->uuidToHex())); entry2->setUrl("http://{S:IpAddress}:{S:Port}/{S:Uri}"); entry2->attributes()->set("CustomUserNameAttribute", "CustomUserNameValue"); entry2->attributes()->set("IpAddress", "127.0.0.1"); @@ -235,10 +234,10 @@ void TestEntry::testResolveRecursivePlaceholders() auto* entry3 = new Entry(); entry3->setGroup(root); entry3->setUuid(QUuid::createUuid()); - entry3->setTitle(QString("{REF:T@I:%1}").arg(QString(entry2->uuid().toRfc4122().toHex()))); - entry3->setUsername(QString("{REF:U@I:%1}").arg(QString(entry2->uuid().toRfc4122().toHex()))); - entry3->setPassword(QString("{REF:P@I:%1}").arg(QString(entry2->uuid().toRfc4122().toHex()))); - entry3->setUrl(QString("{REF:A@I:%1}").arg(QString(entry2->uuid().toRfc4122().toHex()))); + entry3->setTitle(QString("{REF:T@I:%1}").arg(entry2->uuidToHex())); + entry3->setUsername(QString("{REF:U@I:%1}").arg(entry2->uuidToHex())); + entry3->setPassword(QString("{REF:P@I:%1}").arg(entry2->uuidToHex())); + entry3->setUrl(QString("{REF:A@I:%1}").arg(entry2->uuidToHex())); QCOMPARE(entry3->resolveMultiplePlaceholders(entry3->title()), QString("Entry2Title")); QCOMPARE(entry3->resolveMultiplePlaceholders(entry3->username()), QString("CustomUserNameValue")); @@ -248,10 +247,10 @@ void TestEntry::testResolveRecursivePlaceholders() auto* entry4 = new Entry(); entry4->setGroup(root); entry4->setUuid(QUuid::createUuid()); - entry4->setTitle(QString("{REF:T@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex()))); - entry4->setUsername(QString("{REF:U@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex()))); - entry4->setPassword(QString("{REF:P@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex()))); - entry4->setUrl(QString("{REF:A@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex()))); + entry4->setTitle(QString("{REF:T@I:%1}").arg(entry3->uuidToHex())); + entry4->setUsername(QString("{REF:U@I:%1}").arg(entry3->uuidToHex())); + entry4->setPassword(QString("{REF:P@I:%1}").arg(entry3->uuidToHex())); + entry4->setUrl(QString("{REF:A@I:%1}").arg(entry3->uuidToHex())); QCOMPARE(entry4->resolveMultiplePlaceholders(entry4->title()), QString("Entry2Title")); QCOMPARE(entry4->resolveMultiplePlaceholders(entry4->username()), QString("CustomUserNameValue")); @@ -279,7 +278,7 @@ void TestEntry::testResolveRecursivePlaceholders() auto* entry6 = new Entry(); entry6->setGroup(root); entry6->setUuid(QUuid::createUuid()); - entry6->setTitle(QString("{REF:T@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex()))); + entry6->setTitle(QString("{REF:T@I:%1}").arg(entry3->uuidToHex())); entry6->setUsername(QString("{TITLE}")); entry6->setPassword(QString("{PASSWORD}")); @@ -290,7 +289,7 @@ void TestEntry::testResolveRecursivePlaceholders() auto* entry7 = new Entry(); entry7->setGroup(root); entry7->setUuid(QUuid::createUuid()); - entry7->setTitle(QString("{REF:T@I:%1} and something else").arg(QString(entry3->uuid().toRfc4122().toHex()))); + entry7->setTitle(QString("{REF:T@I:%1} and something else").arg(entry3->uuidToHex())); entry7->setUsername(QString("{TITLE}")); entry7->setPassword(QString("PASSWORD")); @@ -344,7 +343,7 @@ void TestEntry::testResolveReferencePlaceholders() tstEntry->setGroup(root); tstEntry->setUuid(QUuid::createUuid()); - QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(QString(entry1->uuid().toRfc4122().toHex()))), + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(entry1->uuidToHex())), entry1->title()); QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@T:%1}").arg(entry1->title())), entry1->title()); QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@U:%1}").arg(entry1->username())), entry1->title()); @@ -355,7 +354,7 @@ void TestEntry::testResolveReferencePlaceholders() QString("{REF:T@O:%1}").arg(entry1->attributes()->value("CustomAttribute1"))), entry1->title()); - QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(QString(entry1->uuid().toRfc4122().toHex()))), + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(entry1->uuidToHex())), entry1->title()); QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@T:%1}").arg(entry1->title())), entry1->title()); QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:U@U:%1}").arg(entry1->username())), @@ -365,7 +364,7 @@ void TestEntry::testResolveReferencePlaceholders() QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:A@A:%1}").arg(entry1->url())), entry1->url()); QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:N@N:%1}").arg(entry1->notes())), entry1->notes()); - QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(QString(entry2->uuid().toRfc4122().toHex()))), + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(entry2->uuidToHex())), entry2->title()); QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@T:%1}").arg(entry2->title())), entry2->title()); QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@U:%1}").arg(entry2->username())), entry2->title()); @@ -384,23 +383,23 @@ void TestEntry::testResolveReferencePlaceholders() QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:A@A:%1}").arg(entry2->url())), entry2->url()); QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:N@N:%1}").arg(entry2->notes())), entry2->notes()); - QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex()))), entry3->attributes()->value("AttributeTitle")); - QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:U@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex()))), entry3->attributes()->value("AttributeUsername")); - QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:P@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex()))), entry3->attributes()->value("AttributePassword")); - QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:A@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex()))), entry3->attributes()->value("AttributeUrl")); - QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:N@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex()))), entry3->attributes()->value("AttributeNotes")); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(entry3->uuidToHex())), entry3->attributes()->value("AttributeTitle")); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:U@I:%1}").arg(entry3->uuidToHex())), entry3->attributes()->value("AttributeUsername")); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:P@I:%1}").arg(entry3->uuidToHex())), entry3->attributes()->value("AttributePassword")); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:A@I:%1}").arg(entry3->uuidToHex())), entry3->attributes()->value("AttributeUrl")); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:N@I:%1}").arg(entry3->uuidToHex())), entry3->attributes()->value("AttributeNotes")); - QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex().toUpper()))), entry3->attributes()->value("AttributeTitle")); - QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:U@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex().toUpper()))), entry3->attributes()->value("AttributeUsername")); - QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:P@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex().toUpper()))), entry3->attributes()->value("AttributePassword")); - QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:A@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex().toUpper()))), entry3->attributes()->value("AttributeUrl")); - QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:N@I:%1}").arg(QString(entry3->uuid().toRfc4122().toHex().toUpper()))), entry3->attributes()->value("AttributeNotes")); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(entry3->uuidToHex().toUpper())), entry3->attributes()->value("AttributeTitle")); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:U@I:%1}").arg(entry3->uuidToHex().toUpper())), entry3->attributes()->value("AttributeUsername")); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:P@I:%1}").arg(entry3->uuidToHex().toUpper())), entry3->attributes()->value("AttributePassword")); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:A@I:%1}").arg(entry3->uuidToHex().toUpper())), entry3->attributes()->value("AttributeUrl")); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:N@I:%1}").arg(entry3->uuidToHex().toUpper())), entry3->attributes()->value("AttributeNotes")); - QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:t@i:%1}").arg(QString(entry3->uuid().toRfc4122().toHex().toLower()))), entry3->attributes()->value("AttributeTitle")); - QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:u@i:%1}").arg(QString(entry3->uuid().toRfc4122().toHex().toLower()))), entry3->attributes()->value("AttributeUsername")); - QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:p@i:%1}").arg(QString(entry3->uuid().toRfc4122().toHex().toLower()))), entry3->attributes()->value("AttributePassword")); - QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:a@i:%1}").arg(QString(entry3->uuid().toRfc4122().toHex().toLower()))), entry3->attributes()->value("AttributeUrl")); - QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:n@i:%1}").arg(QString(entry3->uuid().toRfc4122().toHex().toLower()))), entry3->attributes()->value("AttributeNotes")); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:t@i:%1}").arg(entry3->uuidToHex().toLower())), entry3->attributes()->value("AttributeTitle")); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:u@i:%1}").arg(entry3->uuidToHex().toLower())), entry3->attributes()->value("AttributeUsername")); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:p@i:%1}").arg(entry3->uuidToHex().toLower())), entry3->attributes()->value("AttributePassword")); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:a@i:%1}").arg(entry3->uuidToHex().toLower())), entry3->attributes()->value("AttributeUrl")); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:n@i:%1}").arg(entry3->uuidToHex().toLower())), entry3->attributes()->value("AttributeNotes")); } void TestEntry::testResolveNonIdPlaceholdersToUuid() @@ -469,7 +468,7 @@ void TestEntry::testResolveNonIdPlaceholdersToUuid() const QString newEntryNotesResolved = newEntry->resolveMultiplePlaceholders(newEntry->notes()); - QCOMPARE(newEntryNotesResolved, QString(referencedEntry->uuid().toRfc4122().toHex())); + QCOMPARE(newEntryNotesResolved, referencedEntry->uuidToHex()); } } diff --git a/tests/TestGlobal.h b/tests/TestGlobal.h index 9889a4434..958034293 100644 --- a/tests/TestGlobal.h +++ b/tests/TestGlobal.h @@ -43,14 +43,4 @@ namespace QTest } // namespace QTest -namespace Test -{ - - inline QDateTime datetime(int year, int month, int day, int hour, int min, int second) - { - return QDateTime(QDate(year, month, day), QTime(hour, min, second), Qt::UTC); - } - -} // namespace Test - #endif // KEEPASSXC_TESTGLOBAL_H diff --git a/tests/TestGroup.cpp b/tests/TestGroup.cpp index efdff3168..e97f7ac25 100644 --- a/tests/TestGroup.cpp +++ b/tests/TestGroup.cpp @@ -18,6 +18,7 @@ #include "TestGroup.h" #include "TestGlobal.h" +#include "stub/TestClock.h" #include @@ -26,6 +27,11 @@ QTEST_GUILESS_MAIN(TestGroup) +namespace +{ + TestClock* m_clock = nullptr; +} + void TestGroup::initTestCase() { qRegisterMetaType("Entry*"); @@ -33,6 +39,19 @@ void TestGroup::initTestCase() QVERIFY(Crypto::init()); } +void TestGroup::init() +{ + Q_ASSERT(m_clock == nullptr); + m_clock = new TestClock(2010, 5, 5, 10, 30, 10); + TestClock::setup(m_clock); +} + +void TestGroup::cleanup() +{ + TestClock::teardown(); + m_clock = nullptr; +} + void TestGroup::testParenting() { Database* db = new Database(); @@ -389,7 +408,7 @@ void TestGroup::testClone() QVERIFY(clonedGroupNewUuid->uuid() != originalGroup->uuid()); // Making sure the new modification date is not the same. - QTest::qSleep(1); + m_clock->advanceSecond(1); QScopedPointer clonedGroupResetTimeInfo( originalGroup->clone(Entry::CloneNoFlags, Group::CloneNewUuid | Group::CloneResetTimeInfo)); @@ -474,7 +493,7 @@ void TestGroup::testFindEntry() Entry* entry; - entry = db->rootGroup()->findEntry(entry1->uuid().toRfc4122().toHex()); + entry = db->rootGroup()->findEntry(entry1->uuidToHex()); QVERIFY(entry != nullptr); QCOMPARE(entry->title(), QString("entry1")); @@ -491,7 +510,7 @@ void TestGroup::testFindEntry() entry = db->rootGroup()->findEntry(QString("//entry1")); QVERIFY(entry == nullptr); - entry = db->rootGroup()->findEntry(entry2->uuid().toRfc4122().toHex()); + entry = db->rootGroup()->findEntry(entry2->uuidToHex()); QVERIFY(entry != nullptr); QCOMPARE(entry->title(), QString("entry2")); diff --git a/tests/TestGroup.h b/tests/TestGroup.h index f11cbf6f7..c6ccb21f9 100644 --- a/tests/TestGroup.h +++ b/tests/TestGroup.h @@ -28,6 +28,8 @@ class TestGroup : public QObject private slots: void initTestCase(); + void init(); + void cleanup(); void testParenting(); void testSignals(); void testEntries(); diff --git a/tests/TestKeePass2Format.cpp b/tests/TestKeePass2Format.cpp index 37b5b7838..201c4a64a 100644 --- a/tests/TestKeePass2Format.cpp +++ b/tests/TestKeePass2Format.cpp @@ -17,6 +17,7 @@ #include "TestKeePass2Format.h" #include "TestGlobal.h" +#include "stub/TestClock.h" #include "core/Metadata.h" #include "crypto/Crypto.h" @@ -77,14 +78,14 @@ void TestKeePass2Format::testXmlMetadata() { QCOMPARE(m_xmlDb->metadata()->generator(), QString("KeePass")); QCOMPARE(m_xmlDb->metadata()->name(), QString("ANAME")); - QCOMPARE(m_xmlDb->metadata()->nameChanged(), Test::datetime(2010, 8, 8, 17, 24, 53)); + QCOMPARE(m_xmlDb->metadata()->nameChanged(), TestClock::datetimeUtc(2010, 8, 8, 17, 24, 53)); QCOMPARE(m_xmlDb->metadata()->description(), QString("ADESC")); - QCOMPARE(m_xmlDb->metadata()->descriptionChanged(), Test::datetime(2010, 8, 8, 17, 27, 12)); + QCOMPARE(m_xmlDb->metadata()->descriptionChanged(), TestClock::datetimeUtc(2010, 8, 8, 17, 27, 12)); QCOMPARE(m_xmlDb->metadata()->defaultUserName(), QString("DEFUSERNAME")); - QCOMPARE(m_xmlDb->metadata()->defaultUserNameChanged(), Test::datetime(2010, 8, 8, 17, 27, 45)); + QCOMPARE(m_xmlDb->metadata()->defaultUserNameChanged(), TestClock::datetimeUtc(2010, 8, 8, 17, 27, 45)); QCOMPARE(m_xmlDb->metadata()->maintenanceHistoryDays(), 127); QCOMPARE(m_xmlDb->metadata()->color(), QColor(0xff, 0xef, 0x00)); - QCOMPARE(m_xmlDb->metadata()->masterKeyChanged(), Test::datetime(2012, 4, 5, 17, 9, 34)); + QCOMPARE(m_xmlDb->metadata()->masterKeyChanged(), TestClock::datetimeUtc(2012, 4, 5, 17, 9, 34)); QCOMPARE(m_xmlDb->metadata()->masterKeyChangeRec(), 101); QCOMPARE(m_xmlDb->metadata()->masterKeyChangeForce(), -1); QCOMPARE(m_xmlDb->metadata()->protectTitle(), false); @@ -95,9 +96,9 @@ void TestKeePass2Format::testXmlMetadata() QCOMPARE(m_xmlDb->metadata()->recycleBinEnabled(), true); QVERIFY(m_xmlDb->metadata()->recycleBin() != nullptr); QCOMPARE(m_xmlDb->metadata()->recycleBin()->name(), QString("Recycle Bin")); - QCOMPARE(m_xmlDb->metadata()->recycleBinChanged(), Test::datetime(2010, 8, 25, 16, 12, 57)); + QCOMPARE(m_xmlDb->metadata()->recycleBinChanged(), TestClock::datetimeUtc(2010, 8, 25, 16, 12, 57)); QVERIFY(m_xmlDb->metadata()->entryTemplatesGroup() == nullptr); - QCOMPARE(m_xmlDb->metadata()->entryTemplatesGroupChanged(), Test::datetime(2010, 8, 8, 17, 24, 19)); + QCOMPARE(m_xmlDb->metadata()->entryTemplatesGroupChanged(), TestClock::datetimeUtc(2010, 8, 8, 17, 24, 19)); QVERIFY(m_xmlDb->metadata()->lastSelectedGroup() != nullptr); QCOMPARE(m_xmlDb->metadata()->lastSelectedGroup()->name(), QString("NewDatabase")); QVERIFY(m_xmlDb->metadata()->lastTopVisibleGroup() == m_xmlDb->metadata()->lastSelectedGroup()); @@ -135,13 +136,13 @@ void TestKeePass2Format::testXmlGroupRoot() QCOMPARE(group->iconUuid(), QUuid()); QVERIFY(group->isExpanded()); TimeInfo ti = group->timeInfo(); - QCOMPARE(ti.lastModificationTime(), Test::datetime(2010, 8, 8, 17, 24, 27)); - QCOMPARE(ti.creationTime(), Test::datetime(2010, 8, 7, 17, 24, 27)); - QCOMPARE(ti.lastAccessTime(), Test::datetime(2010, 8, 9, 9, 9, 44)); - QCOMPARE(ti.expiryTime(), Test::datetime(2010, 8, 8, 17, 24, 17)); + QCOMPARE(ti.lastModificationTime(), TestClock::datetimeUtc(2010, 8, 8, 17, 24, 27)); + QCOMPARE(ti.creationTime(), TestClock::datetimeUtc(2010, 8, 7, 17, 24, 27)); + QCOMPARE(ti.lastAccessTime(), TestClock::datetimeUtc(2010, 8, 9, 9, 9, 44)); + QCOMPARE(ti.expiryTime(), TestClock::datetimeUtc(2010, 8, 8, 17, 24, 17)); QVERIFY(!ti.expires()); QCOMPARE(ti.usageCount(), 52); - QCOMPARE(ti.locationChanged(), Test::datetime(2010, 8, 8, 17, 24, 27)); + QCOMPARE(ti.locationChanged(), TestClock::datetimeUtc(2010, 8, 8, 17, 24, 27)); QCOMPARE(group->defaultAutoTypeSequence(), QString("")); QCOMPARE(group->autoTypeEnabled(), Group::Inherit); QCOMPARE(group->searchingEnabled(), Group::Inherit); @@ -202,13 +203,13 @@ void TestKeePass2Format::testXmlEntry1() QCOMPARE(entry->tags(), QString("a b c")); const TimeInfo ti = entry->timeInfo(); - QCOMPARE(ti.lastModificationTime(), Test::datetime(2010, 8, 25, 16, 19, 25)); - QCOMPARE(ti.creationTime(), Test::datetime(2010, 8, 25, 16, 13, 54)); - QCOMPARE(ti.lastAccessTime(), Test::datetime(2010, 8, 25, 16, 19, 25)); - QCOMPARE(ti.expiryTime(), Test::datetime(2010, 8, 25, 16, 12, 57)); + QCOMPARE(ti.lastModificationTime(), TestClock::datetimeUtc(2010, 8, 25, 16, 19, 25)); + QCOMPARE(ti.creationTime(), TestClock::datetimeUtc(2010, 8, 25, 16, 13, 54)); + QCOMPARE(ti.lastAccessTime(), TestClock::datetimeUtc(2010, 8, 25, 16, 19, 25)); + QCOMPARE(ti.expiryTime(), TestClock::datetimeUtc(2010, 8, 25, 16, 12, 57)); QVERIFY(!ti.expires()); QCOMPARE(ti.usageCount(), 8); - QCOMPARE(ti.locationChanged(), Test::datetime(2010, 8, 25, 16, 13, 54)); + QCOMPARE(ti.locationChanged(), TestClock::datetimeUtc(2010, 8, 25, 16, 13, 54)); QList attrs = entry->attributes()->keys(); QCOMPARE(entry->attributes()->value("Notes"), QString("Notes")); @@ -307,7 +308,7 @@ void TestKeePass2Format::testXmlEntryHistory() const Entry* entry = entryMain->historyItems().at(0); QCOMPARE(entry->uuid(), entryMain->uuid()); QVERIFY(!entry->parent()); - QCOMPARE(entry->timeInfo().lastModificationTime(), Test::datetime(2010, 8, 25, 16, 13, 54)); + QCOMPARE(entry->timeInfo().lastModificationTime(), TestClock::datetimeUtc(2010, 8, 25, 16, 13, 54)); QCOMPARE(entry->timeInfo().usageCount(), 3); QCOMPARE(entry->title(), QString("Sample Entry")); QCOMPARE(entry->url(), QString("http://www.somesite.com/")); @@ -317,7 +318,7 @@ void TestKeePass2Format::testXmlEntryHistory() const Entry* entry = entryMain->historyItems().at(1); QCOMPARE(entry->uuid(), entryMain->uuid()); QVERIFY(!entry->parent()); - QCOMPARE(entry->timeInfo().lastModificationTime(), Test::datetime(2010, 8, 25, 16, 15, 43)); + QCOMPARE(entry->timeInfo().lastModificationTime(), TestClock::datetimeUtc(2010, 8, 25, 16, 15, 43)); QCOMPARE(entry->timeInfo().usageCount(), 7); QCOMPARE(entry->title(), QString("Sample Entry 1")); QCOMPARE(entry->url(), QString("http://www.somesite.com/")); @@ -331,11 +332,11 @@ void TestKeePass2Format::testXmlDeletedObjects() delObj = objList.takeFirst(); QCOMPARE(delObj.uuid, QUuid::fromRfc4122(QByteArray::fromBase64("5K/bzWCSmkCv5OZxYl4N/w=="))); - QCOMPARE(delObj.deletionTime, Test::datetime(2010, 8, 25, 16, 14, 12)); + QCOMPARE(delObj.deletionTime, TestClock::datetimeUtc(2010, 8, 25, 16, 14, 12)); delObj = objList.takeFirst(); QCOMPARE(delObj.uuid, QUuid::fromRfc4122(QByteArray::fromBase64("80h8uSNWgkKhKCp1TgXF7g=="))); - QCOMPARE(delObj.deletionTime, Test::datetime(2010, 8, 25, 16, 14, 14)); + QCOMPARE(delObj.deletionTime, TestClock::datetimeUtc(2010, 8, 25, 16, 14, 14)); QVERIFY(objList.isEmpty()); } diff --git a/tests/TestMerge.cpp b/tests/TestMerge.cpp index 278c3001d..0da304f07 100644 --- a/tests/TestMerge.cpp +++ b/tests/TestMerge.cpp @@ -17,12 +17,26 @@ #include "TestMerge.h" #include "TestGlobal.h" +#include "stub/TestClock.h" +#include "core/Merger.h" #include "core/Metadata.h" #include "crypto/Crypto.h" QTEST_GUILESS_MAIN(TestMerge) +namespace +{ + TimeInfo modificationTime(TimeInfo timeInfo, int years, int months, int days) + { + const QDateTime time = timeInfo.lastModificationTime(); + timeInfo.setLastModificationTime(time.addYears(years).addMonths(months).addDays(days)); + return timeInfo; + } + + TestClock* m_clock = nullptr; +} + void TestMerge::initTestCase() { qRegisterMetaType("Entry*"); @@ -30,6 +44,19 @@ void TestMerge::initTestCase() QVERIFY(Crypto::init()); } +void TestMerge::init() +{ + Q_ASSERT(m_clock == nullptr); + m_clock = new TestClock(2010, 5, 5, 10, 30, 10); + TestClock::setup(m_clock); +} + +void TestMerge::cleanup() +{ + TestClock::teardown(); + m_clock = nullptr; +} + /** * Merge an existing database into a new one. * All the entries of the existing should end @@ -37,18 +64,16 @@ void TestMerge::initTestCase() */ void TestMerge::testMergeIntoNew() { - Database* dbSource = createTestDatabase(); - Database* dbDestination = new Database(); + QScopedPointer dbSource(createTestDatabase()); + QScopedPointer dbDestination(new Database()); - dbDestination->merge(dbSource); + Merger merger(dbSource.data(), dbDestination.data()); + merger.merge(); QCOMPARE(dbDestination->rootGroup()->children().size(), 2); QCOMPARE(dbDestination->rootGroup()->children().at(0)->entries().size(), 2); // Test for retention of history QCOMPARE(dbDestination->rootGroup()->children().at(0)->entries().at(0)->historyItems().isEmpty(), false); - - delete dbDestination; - delete dbSource; } /** @@ -57,26 +82,28 @@ void TestMerge::testMergeIntoNew() */ void TestMerge::testMergeNoChanges() { - Database* dbDestination = createTestDatabase(); - - Database* dbSource = new Database(); - dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries)); + QScopedPointer dbDestination(createTestDatabase()); + QScopedPointer dbSource( + createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries)); QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2); QCOMPARE(dbSource->rootGroup()->entriesRecursive().size(), 2); - dbDestination->merge(dbSource); + m_clock->advanceSecond(1); + + Merger merger1(dbSource.data(), dbDestination.data()); + merger1.merge(); QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2); QCOMPARE(dbSource->rootGroup()->entriesRecursive().size(), 2); - dbDestination->merge(dbSource); + m_clock->advanceSecond(1); + + Merger merger2(dbSource.data(), dbDestination.data()); + merger2.merge(); QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2); QCOMPARE(dbSource->rootGroup()->entriesRecursive().size(), 2); - - delete dbDestination; - delete dbSource; } /** @@ -85,46 +112,64 @@ void TestMerge::testMergeNoChanges() */ void TestMerge::testResolveConflictNewer() { - Database* dbDestination = createTestDatabase(); - - Database* dbSource = new Database(); - dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries)); + QScopedPointer dbDestination(createTestDatabase()); + QScopedPointer dbSource( + createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries)); // sanity check - Group* group1 = dbSource->rootGroup()->findChildByName("group1"); - QVERIFY(group1 != nullptr); - QCOMPARE(group1->entries().size(), 2); + QPointer groupSourceInitial = dbSource->rootGroup()->findChildByName("group1"); + QVERIFY(groupSourceInitial != nullptr); + QCOMPARE(groupSourceInitial->entries().size(), 2); - Entry* entry1 = dbSource->rootGroup()->findEntry("entry1"); - QVERIFY(entry1 != nullptr); + QPointer groupDestinationInitial = dbSource->rootGroup()->findChildByName("group1"); + QVERIFY(groupDestinationInitial != nullptr); + QCOMPARE(groupDestinationInitial->entries().size(), 2); + + QPointer entrySourceInitial = dbSource->rootGroup()->findEntry("entry1"); + QVERIFY(entrySourceInitial != nullptr); + QVERIFY(entrySourceInitial->group() == groupSourceInitial); + + const TimeInfo entrySourceInitialTimeInfo = entrySourceInitial->timeInfo(); + const TimeInfo groupSourceInitialTimeInfo = groupSourceInitial->timeInfo(); + const TimeInfo groupDestinationInitialTimeInfo = groupDestinationInitial->timeInfo(); // Make sure the two changes have a different timestamp. - QTest::qSleep(1); + m_clock->advanceSecond(1); // make this entry newer than in destination db - entry1->beginUpdate(); - entry1->setPassword("password"); - entry1->endUpdate(); + entrySourceInitial->beginUpdate(); + entrySourceInitial->setPassword("password"); + entrySourceInitial->endUpdate(); - dbDestination->merge(dbSource); + const TimeInfo entrySourceUpdatedTimeInfo = entrySourceInitial->timeInfo(); + const TimeInfo groupSourceUpdatedTimeInfo = groupSourceInitial->timeInfo(); + + QVERIFY(entrySourceInitialTimeInfo != entrySourceUpdatedTimeInfo); + QVERIFY(groupSourceInitialTimeInfo == groupSourceUpdatedTimeInfo); + QVERIFY(groupSourceInitialTimeInfo == groupDestinationInitialTimeInfo); + + // Make sure the merge changes have a different timestamp. + m_clock->advanceSecond(1); + + Merger merger(dbSource.data(), dbDestination.data()); + merger.merge(); // sanity check - group1 = dbDestination->rootGroup()->findChildByName("group1"); - QVERIFY(group1 != nullptr); - QCOMPARE(group1->entries().size(), 2); + QPointer groupDestinationMerged = dbDestination->rootGroup()->findChildByName("group1"); + QVERIFY(groupDestinationMerged != nullptr); + QCOMPARE(groupDestinationMerged->entries().size(), 2); + QCOMPARE(groupDestinationMerged->timeInfo(), groupDestinationInitialTimeInfo); - entry1 = dbDestination->rootGroup()->findEntry("entry1"); - QVERIFY(entry1 != nullptr); - QVERIFY(entry1->group() != nullptr); - QCOMPARE(entry1->password(), QString("password")); + QPointer entryDestinationMerged = dbDestination->rootGroup()->findEntry("entry1"); + QVERIFY(entryDestinationMerged != nullptr); + QVERIFY(entryDestinationMerged->group() != nullptr); + QCOMPARE(entryDestinationMerged->password(), QString("password")); + QCOMPARE(entryDestinationMerged->timeInfo(), entrySourceUpdatedTimeInfo); // When updating an entry, it should not end up in the // deleted objects. for (DeletedObject deletedObject : dbDestination->deletedObjects()) { - QVERIFY(deletedObject.uuid != entry1->uuid()); + QVERIFY(deletedObject.uuid != entryDestinationMerged->uuid()); } - - delete dbDestination; - delete dbSource; } /** @@ -132,94 +177,607 @@ void TestMerge::testResolveConflictNewer() * destination database after, the entry should remain the * same. */ -void TestMerge::testResolveConflictOlder() +void TestMerge::testResolveConflictExisting() { - Database* dbDestination = createTestDatabase(); - - Database* dbSource = new Database(); - dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries)); + QScopedPointer dbDestination(createTestDatabase()); + QScopedPointer dbSource( + createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries)); // sanity check - Group* group1 = dbSource->rootGroup()->findChildByName("group1"); - QVERIFY(group1 != nullptr); - QCOMPARE(group1->entries().size(), 2); + QPointer groupSourceInitial = dbSource->rootGroup()->findChildByName("group1"); + QVERIFY(groupSourceInitial != nullptr); + QCOMPARE(groupSourceInitial->entries().size(), 2); - Entry* entry1 = dbSource->rootGroup()->findEntry("entry1"); - QVERIFY(entry1 != nullptr); + QPointer groupDestinationInitial = dbDestination->rootGroup()->findChildByName("group1"); + QVERIFY(groupDestinationInitial != nullptr); + QCOMPARE(groupSourceInitial->entries().size(), 2); + + QPointer entrySourceInitial = dbSource->rootGroup()->findEntry("entry1"); + QVERIFY(entrySourceInitial != nullptr); + QVERIFY(entrySourceInitial->group() == groupSourceInitial); + + const TimeInfo entrySourceInitialTimeInfo = entrySourceInitial->timeInfo(); + const TimeInfo groupSourceInitialTimeInfo = groupSourceInitial->timeInfo(); + const TimeInfo groupDestinationInitialTimeInfo = groupDestinationInitial->timeInfo(); // 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(); + m_clock->advanceSecond(1); + // make this entry older than in destination db + entrySourceInitial->beginUpdate(); + entrySourceInitial->setPassword("password1"); + entrySourceInitial->endUpdate(); - entry1 = dbDestination->rootGroup()->findEntry("entry1"); - QVERIFY(entry1 != nullptr); + const TimeInfo entrySourceUpdatedOlderTimeInfo = entrySourceInitial->timeInfo(); + const TimeInfo groupSourceUpdatedOlderTimeInfo = groupSourceInitial->timeInfo(); + + QPointer groupDestinationUpdated = dbDestination->rootGroup()->findChildByName("group1"); + QVERIFY(groupDestinationUpdated != nullptr); + QCOMPARE(groupDestinationUpdated->entries().size(), 2); + QPointer entryDestinationUpdated = dbDestination->rootGroup()->findEntry("entry1"); + QVERIFY(entryDestinationUpdated != nullptr); + QVERIFY(entryDestinationUpdated->group() == groupDestinationUpdated); // 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(); + m_clock->advanceSecond(1); + // make this entry newer than in source db + entryDestinationUpdated->beginUpdate(); + entryDestinationUpdated->setPassword("password2"); + entryDestinationUpdated->endUpdate(); - dbDestination->merge(dbSource); + const TimeInfo entryDestinationUpdatedNewerTimeInfo = entryDestinationUpdated->timeInfo(); + const TimeInfo groupDestinationUpdatedNewerTimeInfo = groupDestinationUpdated->timeInfo(); + QVERIFY(entrySourceUpdatedOlderTimeInfo != entrySourceInitialTimeInfo); + QVERIFY(entrySourceUpdatedOlderTimeInfo != entryDestinationUpdatedNewerTimeInfo); + QVERIFY(groupSourceInitialTimeInfo == groupSourceUpdatedOlderTimeInfo); + QVERIFY(groupDestinationInitialTimeInfo == groupDestinationUpdatedNewerTimeInfo); + QVERIFY(groupSourceInitialTimeInfo == groupDestinationInitialTimeInfo); + + // Make sure the merge changes have a different timestamp. + m_clock->advanceSecond(1); + + Merger merger(dbSource.data(), dbDestination.data()); + merger.merge(); // sanity check - group1 = dbDestination->rootGroup()->findChildByName("group1"); - QVERIFY(group1 != nullptr); - QCOMPARE(group1->entries().size(), 2); + QPointer groupDestinationMerged = dbDestination->rootGroup()->findChildByName("group1"); + QVERIFY(groupDestinationMerged != nullptr); + QCOMPARE(groupDestinationMerged->entries().size(), 2); + QCOMPARE(groupDestinationMerged->timeInfo(), groupDestinationUpdatedNewerTimeInfo); - entry1 = dbDestination->rootGroup()->findEntry("entry1"); - QVERIFY(entry1 != nullptr); - QCOMPARE(entry1->password(), QString("password2")); + QPointer entryDestinationMerged = dbDestination->rootGroup()->findEntry("entry1"); + QVERIFY(entryDestinationMerged != nullptr); + QCOMPARE(entryDestinationMerged->password(), QString("password2")); + QCOMPARE(entryDestinationMerged->timeInfo(), entryDestinationUpdatedNewerTimeInfo); // When updating an entry, it should not end up in the // deleted objects. for (DeletedObject deletedObject : dbDestination->deletedObjects()) { - QVERIFY(deletedObject.uuid != entry1->uuid()); + QVERIFY(deletedObject.uuid != entryDestinationMerged->uuid()); } - - delete dbDestination; - delete dbSource; } /** * Tests the KeepBoth merge mode. */ -void TestMerge::testResolveConflictKeepBoth() +void TestMerge::testResolveConflictDuplicate() { - Database* dbDestination = createTestDatabase(); - - Database* dbSource = new Database(); - dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneIncludeHistory, Group::CloneIncludeEntries)); + QScopedPointer dbDestination(createTestDatabase()); + QScopedPointer dbSource( + createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneIncludeHistory, Group::CloneIncludeEntries)); // 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); + QPointer updatedDestinationEntry = dbDestination->rootGroup()->children().at(0)->entries().at(0); + const TimeInfo initialEntryTimeInfo = updatedDestinationEntry->timeInfo(); + const TimeInfo updatedEntryTimeInfo = modificationTime(initialEntryTimeInfo, 1, 0, 0); - dbDestination->rootGroup()->setMergeMode(Group::MergeMode::KeepBoth); + updatedDestinationEntry->setTimeInfo(updatedEntryTimeInfo); - dbDestination->merge(dbSource); + dbDestination->rootGroup()->setMergeMode(Group::MergeMode::Duplicate); + + // Make sure the merge changes have a different timestamp. + m_clock->advanceSecond(1); + + Merger merger(dbSource.data(), dbDestination.data()); + merger.merge(); // one entry is duplicated because of mode QCOMPARE(dbDestination->rootGroup()->children().at(0)->entries().size(), 3); QCOMPARE(dbDestination->rootGroup()->children().at(0)->entries().at(0)->historyItems().isEmpty(), false); // the older entry was merged from the other db as last in the group - Entry* olderEntry = dbDestination->rootGroup()->children().at(0)->entries().at(2); + QPointer newerEntry = dbDestination->rootGroup()->children().at(0)->entries().at(0); + QPointer olderEntry = dbDestination->rootGroup()->children().at(0)->entries().at(2); + QVERIFY(newerEntry->title() == olderEntry->title()); + QVERIFY2(!newerEntry->attributes()->hasKey("merged"), "newer entry is not marked with an attribute \"merged\""); QVERIFY2(olderEntry->attributes()->hasKey("merged"), "older entry is marked with an attribute \"merged\""); QCOMPARE(olderEntry->historyItems().isEmpty(), false); + QCOMPARE(newerEntry->timeInfo(), updatedEntryTimeInfo); + // TODO HNH: this may be subject to discussions since the entry itself is newer but represents an older one + // QCOMPARE(olderEntry->timeInfo(), initialEntryTimeInfo); + QVERIFY2(olderEntry->uuidToHex() != updatedDestinationEntry->uuidToHex(), + "KeepBoth should not reuse the UUIDs when cloning."); +} - QVERIFY2(olderEntry->uuid() != updatedEntry->uuid(), "KeepBoth should not reuse the UUIDs when cloning."); +void TestMerge::testResolveConflictTemplate(int mergeMode, std::function&)> verification) +{ + QMap timestamps; + timestamps["initialTime"] = m_clock->currentDateTimeUtc(); + QScopedPointer dbDestination(createTestDatabase()); - delete dbSource; - delete dbDestination; + Entry* deletedEntry1 = new Entry(); + deletedEntry1->setUuid(QUuid::createUuid()); + + deletedEntry1->beginUpdate(); + deletedEntry1->setGroup(dbDestination->rootGroup()); + deletedEntry1->setTitle("deletedDestination"); + deletedEntry1->endUpdate(); + + Entry* deletedEntry2 = new Entry(); + deletedEntry2->setUuid(QUuid::createUuid()); + + deletedEntry2->beginUpdate(); + deletedEntry2->setGroup(dbDestination->rootGroup()); + deletedEntry2->setTitle("deletedSource"); + deletedEntry2->endUpdate(); + + QScopedPointer dbSource( + createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneIncludeHistory, Group::CloneIncludeEntries)); + + timestamps["oldestCommonHistoryTime"] = m_clock->currentDateTimeUtc(); + + // sanity check + QCOMPARE(dbDestination->rootGroup()->children().at(0)->entries().size(), 2); + QCOMPARE(dbDestination->rootGroup()->children().at(0)->entries().at(0)->historyItems().count(), 1); + QCOMPARE(dbDestination->rootGroup()->children().at(0)->entries().at(1)->historyItems().count(), 1); + QCOMPARE(dbSource->rootGroup()->children().at(0)->entries().size(), 2); + QCOMPARE(dbSource->rootGroup()->children().at(0)->entries().at(0)->historyItems().count(), 1); + QCOMPARE(dbSource->rootGroup()->children().at(0)->entries().at(1)->historyItems().count(), 1); + + // simulate some work in the dbs (manipulate the history) + QPointer destinationEntry1 = dbDestination->rootGroup()->children().at(0)->entries().at(0); + QPointer destinationEntry2 = dbDestination->rootGroup()->children().at(0)->entries().at(1); + QPointer sourceEntry1 = dbSource->rootGroup()->children().at(0)->entries().at(0); + QPointer sourceEntry2 = dbSource->rootGroup()->children().at(0)->entries().at(1); + + timestamps["newestCommonHistoryTime"] = m_clock->advanceMinute(1); + + destinationEntry1->beginUpdate(); + destinationEntry1->setNotes("1 Common"); + destinationEntry1->endUpdate(); + destinationEntry2->beginUpdate(); + destinationEntry2->setNotes("1 Common"); + destinationEntry2->endUpdate(); + sourceEntry1->beginUpdate(); + sourceEntry1->setNotes("1 Common"); + sourceEntry1->endUpdate(); + sourceEntry2->beginUpdate(); + sourceEntry2->setNotes("1 Common"); + sourceEntry2->endUpdate(); + + timestamps["oldestDivergingHistoryTime"] = m_clock->advanceSecond(1); + + destinationEntry2->beginUpdate(); + destinationEntry2->setNotes("2 Destination"); + destinationEntry2->endUpdate(); + sourceEntry1->beginUpdate(); + sourceEntry1->setNotes("2 Source"); + sourceEntry1->endUpdate(); + + timestamps["newestDivergingHistoryTime"] = m_clock->advanceHour(1); + + destinationEntry1->beginUpdate(); + destinationEntry1->setNotes("3 Destination"); + destinationEntry1->endUpdate(); + sourceEntry2->beginUpdate(); + sourceEntry2->setNotes("3 Source"); + sourceEntry2->endUpdate(); + + // sanity check + QCOMPARE(dbDestination->rootGroup()->children().at(0)->entries().at(0)->historyItems().count(), 3); + QCOMPARE(dbDestination->rootGroup()->children().at(0)->entries().at(1)->historyItems().count(), 3); + QCOMPARE(dbSource->rootGroup()->children().at(0)->entries().at(0)->historyItems().count(), 3); + QCOMPARE(dbSource->rootGroup()->children().at(0)->entries().at(1)->historyItems().count(), 3); + + m_clock->advanceMinute(1); + + QPointer deletedEntryDestination = dbDestination->rootGroup()->findEntry("deletedDestination"); + dbDestination->recycleEntry(deletedEntryDestination); + QPointer deletedEntrySource = dbSource->rootGroup()->findEntry("deletedSource"); + dbSource->recycleEntry(deletedEntrySource); + + m_clock->advanceMinute(1); + + Entry* destinationEntrySingle = new Entry(); + destinationEntrySingle->setUuid(QUuid::createUuid()); + + destinationEntrySingle->beginUpdate(); + destinationEntrySingle->setGroup(dbDestination->rootGroup()->children().at(1)); + destinationEntrySingle->setTitle("entryDestination"); + destinationEntrySingle->endUpdate(); + + Entry* sourceEntrySingle = new Entry(); + sourceEntrySingle->setUuid(QUuid::createUuid()); + + sourceEntrySingle->beginUpdate(); + sourceEntrySingle->setGroup(dbSource->rootGroup()->children().at(1)); + sourceEntrySingle->setTitle("entrySource"); + sourceEntrySingle->endUpdate(); + + dbDestination->rootGroup()->setMergeMode(static_cast(mergeMode)); + + // Make sure the merge changes have a different timestamp. + timestamps["mergeTime"] = m_clock->advanceSecond(1); + + Merger merger(dbSource.data(), dbDestination.data()); + merger.merge(); + + QPointer mergedRootGroup = dbDestination->rootGroup(); + QCOMPARE(mergedRootGroup->entries().size(), 0); + // Both databases contain their own generated recycleBin - just one is considered a real recycleBin, the other + // exists as normal group, therefore only one entry is considered deleted + QCOMPARE(dbDestination->metadata()->recycleBin()->entries().size(), 1); + QPointer mergedGroup1 = mergedRootGroup->children().at(0); + QPointer mergedGroup2 = mergedRootGroup->children().at(1); + QVERIFY(mergedGroup1); + QVERIFY(mergedGroup2); + QCOMPARE(mergedGroup2->entries().size(), 2); + QVERIFY(mergedGroup1->entries().at(0)); + QVERIFY(mergedGroup1->entries().at(1)); + + verification(dbDestination.data(), timestamps); + + QVERIFY(dbDestination->rootGroup()->findEntry("entryDestination")); + QVERIFY(dbDestination->rootGroup()->findEntry("entrySource")); +} + +void TestMerge::testDeletionConflictTemplate(int mergeMode, std::function&)> verification) +{ + QMap identifiers; + m_clock->currentDateTimeUtc(); + QScopedPointer dbDestination(createTestDatabase()); + + // scenarios: + // entry directly deleted in source before updated in target + // entry directly deleted in source after updated in target + // entry directly deleted in target before updated in source + // entry directly deleted in target after updated in source + + // entry indirectly deleted in source before updated in target + // entry indirectly deleted in source after updated in target + // entry indirectly deleted in target before updated in source + // entry indirectly deleted in target after updated in source + + auto createGroup = [&](const char* name, Group* parent) { + Group* group = new Group(); + group->setUuid(QUuid::createUuid()); + group->setName(name); + group->setParent(parent, 0); + identifiers[group->name()] = group->uuid(); + return group; + }; + auto createEntry = [&](const char* title, Group* parent) { + Entry* entry = new Entry(); + entry->setUuid(QUuid::createUuid()); + entry->setTitle(title); + entry->setGroup(parent); + identifiers[entry->title()] = entry->uuid(); + return entry; + }; + auto changeEntry = [](Entry* entry) { + entry->beginUpdate(); + entry->setNotes("Change"); + entry->endUpdate(); + }; + + Group* directlyDeletedEntryGroup = createGroup("DirectlyDeletedEntries", dbDestination->rootGroup()); + createEntry("EntryDeletedInSourceBeforeChangedInTarget", directlyDeletedEntryGroup); + createEntry("EntryDeletedInSourceAfterChangedInTarget", directlyDeletedEntryGroup); + createEntry("EntryDeletedInTargetBeforeChangedInSource", directlyDeletedEntryGroup); + createEntry("EntryDeletedInTargetAfterChangedInSource", directlyDeletedEntryGroup); + + Group* groupDeletedInSourceBeforeEntryUpdatedInTarget = + createGroup("GroupDeletedInSourceBeforeEntryUpdatedInTarget", dbDestination->rootGroup()); + createEntry("EntryDeletedInSourceBeforeEntryUpdatedInTarget", groupDeletedInSourceBeforeEntryUpdatedInTarget); + + Group* groupDeletedInSourceAfterEntryUpdatedInTarget = + createGroup("GroupDeletedInSourceAfterEntryUpdatedInTarget", dbDestination->rootGroup()); + createEntry("EntryDeletedInSourceAfterEntryUpdatedInTarget", groupDeletedInSourceAfterEntryUpdatedInTarget); + + Group* groupDeletedInTargetBeforeEntryUpdatedInSource = + createGroup("GroupDeletedInTargetBeforeEntryUpdatedInSource", dbDestination->rootGroup()); + createEntry("EntryDeletedInTargetBeforeEntryUpdatedInSource", groupDeletedInTargetBeforeEntryUpdatedInSource); + + Group* groupDeletedInTargetAfterEntryUpdatedInSource = + createGroup("GroupDeletedInTargetAfterEntryUpdatedInSource", dbDestination->rootGroup()); + createEntry("EntryDeletedInTargetAfterEntryUpdatedInSource", groupDeletedInTargetAfterEntryUpdatedInSource); + + QScopedPointer dbSource( + createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneIncludeHistory, Group::CloneIncludeEntries)); + + QPointer sourceEntryDeletedInSourceBeforeChangedInTarget = + dbSource->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInSourceBeforeChangedInTarget"]); + QPointer targetEntryDeletedInSourceBeforeChangedInTarget = + dbDestination->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInSourceBeforeChangedInTarget"]); + + QPointer sourceEntryDeletedInSourceAfterChangedInTarget = + dbSource->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInSourceAfterChangedInTarget"]); + QPointer targetEntryDeletedInSourceAfterChangedInTarget = + dbDestination->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInSourceAfterChangedInTarget"]); + + QPointer sourceEntryDeletedInTargetBeforeChangedInSource = + dbSource->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInTargetBeforeChangedInSource"]); + QPointer targetEntryDeletedInTargetBeforeChangedInSource = + dbDestination->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInTargetBeforeChangedInSource"]); + + QPointer sourceEntryDeletedInTargetAfterChangedInSource = + dbSource->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInTargetAfterChangedInSource"]); + QPointer targetEntryDeletedInTargetAfterChangedInSource = + dbDestination->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInTargetAfterChangedInSource"]); + + QPointer sourceGroupDeletedInSourceBeforeEntryUpdatedInTarget = + dbSource->rootGroup()->findGroupByUuid(identifiers["GroupDeletedInSourceBeforeEntryUpdatedInTarget"]); + QPointer targetEntryDeletedInSourceBeforeEntryUpdatedInTarget = + dbDestination->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInSourceBeforeEntryUpdatedInTarget"]); + + QPointer sourceGroupDeletedInSourceAfterEntryUpdatedInTarget = + dbSource->rootGroup()->findGroupByUuid(identifiers["GroupDeletedInSourceAfterEntryUpdatedInTarget"]); + QPointer targetEntryDeletedInSourceAfterEntryUpdatedInTarget = + dbDestination->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInSourceAfterEntryUpdatedInTarget"]); + + QPointer targetGroupDeletedInTargetBeforeEntryUpdatedInSource = + dbDestination->rootGroup()->findGroupByUuid(identifiers["GroupDeletedInTargetBeforeEntryUpdatedInSource"]); + QPointer sourceEntryDeletedInTargetBeforeEntryUpdatedInSource = + dbSource->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInTargetBeforeEntryUpdatedInSource"]); + + QPointer targetGroupDeletedInTargetAfterEntryUpdatedInSource = + dbDestination->rootGroup()->findGroupByUuid(identifiers["GroupDeletedInTargetAfterEntryUpdatedInSource"]); + QPointer sourceEntryDeletedInTargetAfterEntryUpdatedInSoruce = + dbSource->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInTargetAfterEntryUpdatedInSource"]); + + // simulate some work in the dbs (manipulate the history) + m_clock->advanceMinute(1); + + delete sourceEntryDeletedInSourceBeforeChangedInTarget.data(); + changeEntry(targetEntryDeletedInSourceAfterChangedInTarget); + delete targetEntryDeletedInTargetBeforeChangedInSource.data(); + changeEntry(sourceEntryDeletedInTargetAfterChangedInSource); + + delete sourceGroupDeletedInSourceBeforeEntryUpdatedInTarget.data(); + changeEntry(targetEntryDeletedInSourceAfterEntryUpdatedInTarget); + delete targetGroupDeletedInTargetBeforeEntryUpdatedInSource.data(); + changeEntry(sourceEntryDeletedInTargetAfterEntryUpdatedInSoruce); + + m_clock->advanceMinute(1); + + changeEntry(targetEntryDeletedInSourceBeforeChangedInTarget); + delete sourceEntryDeletedInSourceAfterChangedInTarget.data(); + changeEntry(sourceEntryDeletedInTargetBeforeChangedInSource); + delete targetEntryDeletedInTargetAfterChangedInSource.data(); + + changeEntry(targetEntryDeletedInSourceBeforeEntryUpdatedInTarget); + delete sourceGroupDeletedInSourceAfterEntryUpdatedInTarget.data(); + changeEntry(sourceEntryDeletedInTargetBeforeEntryUpdatedInSource); + delete targetGroupDeletedInTargetAfterEntryUpdatedInSource.data(); + m_clock->advanceMinute(1); + + dbDestination->rootGroup()->setMergeMode(static_cast(mergeMode)); + + Merger merger(dbSource.data(), dbDestination.data()); + merger.merge(); + + verification(dbDestination.data(), identifiers); +} + +void TestMerge::assertDeletionNewerOnly(Database* db, const QMap& identifiers) +{ + QPointer mergedRootGroup = db->rootGroup(); + // newer change in target prevents deletion + QVERIFY(mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInSourceBeforeChangedInTarget"])); + QVERIFY(!db->containsDeletedObject(identifiers["EntryDeletedInSourceBeforeChangedInTarget"])); + // newer deletion in source forces deletion + QVERIFY(!mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInSourceAfterChangedInTarget"])); + QVERIFY(db->containsDeletedObject(identifiers["EntryDeletedInSourceAfterChangedInTarget"])); + // newer change in source privents deletion + QVERIFY(mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInTargetBeforeChangedInSource"])); + QVERIFY(!db->containsDeletedObject(identifiers["EntryDeletedInTargetBeforeChangedInSource"])); + // newer deletion in target forces deletion + QVERIFY(!mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInTargetAfterChangedInSource"])); + QVERIFY(db->containsDeletedObject(identifiers["EntryDeletedInTargetAfterChangedInSource"])); + // newer change in target prevents deletion + QVERIFY(mergedRootGroup->findGroupByUuid(identifiers["GroupDeletedInSourceBeforeEntryUpdatedInTarget"])); + QVERIFY(!db->containsDeletedObject(identifiers["GroupDeletedInSourceBeforeEntryUpdatedInTarget"])); + QVERIFY(mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInSourceBeforeEntryUpdatedInTarget"])); + QVERIFY(!db->containsDeletedObject(identifiers["EntryDeletedInSourceBeforeEntryUpdatedInTarget"])); + // newer deletion in source forces deletion + QVERIFY(!mergedRootGroup->findGroupByUuid(identifiers["GroupDeletedInSourceAfterEntryUpdatedInTarget"])); + QVERIFY(db->containsDeletedObject(identifiers["GroupDeletedInSourceAfterEntryUpdatedInTarget"])); + QVERIFY(!mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInSourceAfterEntryUpdatedInTarget"])); + QVERIFY(db->containsDeletedObject(identifiers["EntryDeletedInSourceAfterEntryUpdatedInTarget"])); + // newer change in source privents deletion + QVERIFY(mergedRootGroup->findGroupByUuid(identifiers["GroupDeletedInTargetBeforeEntryUpdatedInSource"])); + QVERIFY(!db->containsDeletedObject(identifiers["GroupDeletedInTargetBeforeEntryUpdatedInSource"])); + QVERIFY(mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInTargetBeforeEntryUpdatedInSource"])); + QVERIFY(!db->containsDeletedObject(identifiers["EntryDeletedInTargetBeforeEntryUpdatedInSource"])); + // newer deletion in target forces deletion + QVERIFY(!mergedRootGroup->findGroupByUuid(identifiers["GroupDeletedInTargetAfterEntryUpdatedInSource"])); + QVERIFY(db->containsDeletedObject(identifiers["GroupDeletedInTargetAfterEntryUpdatedInSource"])); + QVERIFY(!mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInTargetAfterEntryUpdatedInSource"])); + QVERIFY(db->containsDeletedObject(identifiers["EntryDeletedInTargetAfterEntryUpdatedInSource"])); +} + +void TestMerge::assertDeletionLocalOnly(Database* db, const QMap &identifiers) +{ + QPointer mergedRootGroup = db->rootGroup(); + + QVERIFY(mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInSourceBeforeChangedInTarget"])); + QVERIFY(!db->containsDeletedObject(identifiers["EntryDeletedInSourceBeforeChangedInTarget"])); + + QVERIFY(mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInSourceAfterChangedInTarget"])); + QVERIFY(!db->containsDeletedObject(identifiers["EntryDeletedInSourceAfterChangedInTarget"])); + + // Uuids in db and deletedObjects is intended according to KeePass #1752 + QVERIFY(mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInTargetBeforeChangedInSource"])); + QVERIFY(db->containsDeletedObject(identifiers["EntryDeletedInTargetBeforeChangedInSource"])); + + QVERIFY(mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInTargetAfterChangedInSource"])); + QVERIFY(db->containsDeletedObject(identifiers["EntryDeletedInTargetAfterChangedInSource"])); + + QVERIFY(mergedRootGroup->findGroupByUuid(identifiers["GroupDeletedInSourceBeforeEntryUpdatedInTarget"])); + QVERIFY(!db->containsDeletedObject(identifiers["GroupDeletedInSourceBeforeEntryUpdatedInTarget"])); + QVERIFY(mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInSourceBeforeEntryUpdatedInTarget"])); + QVERIFY(!db->containsDeletedObject(identifiers["EntryDeletedInSourceBeforeEntryUpdatedInTarget"])); + + QVERIFY(mergedRootGroup->findGroupByUuid(identifiers["GroupDeletedInSourceAfterEntryUpdatedInTarget"])); + QVERIFY(!db->containsDeletedObject(identifiers["GroupDeletedInSourceAfterEntryUpdatedInTarget"])); + QVERIFY(mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInSourceAfterEntryUpdatedInTarget"])); + QVERIFY(!db->containsDeletedObject(identifiers["EntryDeletedInSourceAfterEntryUpdatedInTarget"])); + + QVERIFY(mergedRootGroup->findGroupByUuid(identifiers["GroupDeletedInTargetBeforeEntryUpdatedInSource"])); + QVERIFY(db->containsDeletedObject(identifiers["GroupDeletedInTargetBeforeEntryUpdatedInSource"])); + QVERIFY(mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInTargetBeforeEntryUpdatedInSource"])); + QVERIFY(db->containsDeletedObject(identifiers["EntryDeletedInTargetBeforeEntryUpdatedInSource"])); + + QVERIFY(mergedRootGroup->findGroupByUuid(identifiers["GroupDeletedInTargetAfterEntryUpdatedInSource"])); + QVERIFY(db->containsDeletedObject(identifiers["GroupDeletedInTargetAfterEntryUpdatedInSource"])); + QVERIFY(mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInTargetAfterEntryUpdatedInSource"])); + QVERIFY(db->containsDeletedObject(identifiers["EntryDeletedInTargetAfterEntryUpdatedInSource"])); +} + +void TestMerge::assertUpdateMergedEntry1(Entry *mergedEntry1, const QMap ×tamps) +{ + QCOMPARE(mergedEntry1->historyItems().count(), 4); + QCOMPARE(mergedEntry1->historyItems().at(0)->notes(), QString("")); + QCOMPARE(mergedEntry1->historyItems().at(0)->timeInfo().lastModificationTime(), timestamps["initialTime"]); + QCOMPARE(mergedEntry1->historyItems().at(1)->notes(), QString("")); + QCOMPARE(mergedEntry1->historyItems().at(1)->timeInfo().lastModificationTime(), timestamps["oldestCommonHistoryTime"]); + QCOMPARE(mergedEntry1->historyItems().at(2)->notes(), QString("1 Common")); + QCOMPARE(mergedEntry1->historyItems().at(2)->timeInfo().lastModificationTime(), timestamps["newestCommonHistoryTime"]); + QCOMPARE(mergedEntry1->historyItems().at(3)->notes(), QString("2 Source")); + QCOMPARE(mergedEntry1->historyItems().at(3)->timeInfo().lastModificationTime(), timestamps["oldestDivergingHistoryTime"]); + QCOMPARE(mergedEntry1->notes(), QString("3 Destination")); + QCOMPARE(mergedEntry1->timeInfo().lastModificationTime(), timestamps["newestDivergingHistoryTime"]); +} + +void TestMerge::assertUpdateReappliedEntry2(Entry *mergedEntry2, const QMap ×tamps) +{ + QCOMPARE(mergedEntry2->historyItems().count(), 5); + QCOMPARE(mergedEntry2->historyItems().at(0)->notes(), QString("")); + QCOMPARE(mergedEntry2->historyItems().at(0)->timeInfo().lastModificationTime(), timestamps["initialTime"]); + QCOMPARE(mergedEntry2->historyItems().at(1)->notes(), QString("")); + QCOMPARE(mergedEntry2->historyItems().at(1)->timeInfo().lastModificationTime(), timestamps["oldestCommonHistoryTime"]); + QCOMPARE(mergedEntry2->historyItems().at(2)->notes(), QString("1 Common")); + QCOMPARE(mergedEntry2->historyItems().at(2)->timeInfo().lastModificationTime(), timestamps["newestCommonHistoryTime"]); + QCOMPARE(mergedEntry2->historyItems().at(3)->notes(), QString("2 Destination")); + QCOMPARE(mergedEntry2->historyItems().at(3)->timeInfo().lastModificationTime(), timestamps["oldestDivergingHistoryTime"]); + QCOMPARE(mergedEntry2->historyItems().at(4)->notes(), QString("3 Source")); + QCOMPARE(mergedEntry2->historyItems().at(4)->timeInfo().lastModificationTime(), timestamps["newestDivergingHistoryTime"]); + QCOMPARE(mergedEntry2->notes(), QString("2 Destination")); + QCOMPARE(mergedEntry2->timeInfo().lastModificationTime(), timestamps["mergeTime"]); +} + +void TestMerge::assertUpdateReappliedEntry1(Entry *mergedEntry1, const QMap ×tamps) +{ + QCOMPARE(mergedEntry1->historyItems().count(), 5); + QCOMPARE(mergedEntry1->historyItems().at(0)->notes(), QString("")); + QCOMPARE(mergedEntry1->historyItems().at(0)->timeInfo().lastModificationTime(), timestamps["initialTime"]); + QCOMPARE(mergedEntry1->historyItems().at(1)->notes(), QString("")); + QCOMPARE(mergedEntry1->historyItems().at(1)->timeInfo().lastModificationTime(), timestamps["oldestCommonHistoryTime"]); + QCOMPARE(mergedEntry1->historyItems().at(2)->notes(), QString("1 Common")); + QCOMPARE(mergedEntry1->historyItems().at(2)->timeInfo().lastModificationTime(), timestamps["newestCommonHistoryTime"]); + QCOMPARE(mergedEntry1->historyItems().at(3)->notes(), QString("2 Source")); + QCOMPARE(mergedEntry1->historyItems().at(3)->timeInfo().lastModificationTime(), timestamps["oldestDivergingHistoryTime"]); + QCOMPARE(mergedEntry1->historyItems().at(4)->notes(), QString("3 Destination")); + QCOMPARE(mergedEntry1->historyItems().at(4)->timeInfo().lastModificationTime(), timestamps["newestDivergingHistoryTime"]); + QCOMPARE(mergedEntry1->notes(), QString("2 Source")); + QCOMPARE(mergedEntry1->timeInfo().lastModificationTime(), timestamps["mergeTime"]); +} + +void TestMerge::assertUpdateMergedEntry2(Entry *mergedEntry2, const QMap ×tamps) +{ + QCOMPARE(mergedEntry2->historyItems().count(), 4); + QCOMPARE(mergedEntry2->historyItems().at(0)->notes(), QString("")); + QCOMPARE(mergedEntry2->historyItems().at(0)->timeInfo().lastModificationTime(), timestamps["initialTime"]); + QCOMPARE(mergedEntry2->historyItems().at(1)->notes(), QString("")); + QCOMPARE(mergedEntry2->historyItems().at(1)->timeInfo().lastModificationTime(), timestamps["oldestCommonHistoryTime"]); + QCOMPARE(mergedEntry2->historyItems().at(2)->notes(), QString("1 Common")); + QCOMPARE(mergedEntry2->historyItems().at(2)->timeInfo().lastModificationTime(), timestamps["newestCommonHistoryTime"]); + QCOMPARE(mergedEntry2->historyItems().at(3)->notes(), QString("2 Destination")); + QCOMPARE(mergedEntry2->historyItems().at(3)->timeInfo().lastModificationTime(), timestamps["oldestDivergingHistoryTime"]); + QCOMPARE(mergedEntry2->notes(), QString("3 Source")); + QCOMPARE(mergedEntry2->timeInfo().lastModificationTime(), timestamps["newestDivergingHistoryTime"]); +} + +void TestMerge::testDeletionConflictEntry_Synchronized() +{ + testDeletionConflictTemplate(Group::Synchronize, &TestMerge::assertDeletionNewerOnly); +} + +void TestMerge::testDeletionConflictEntry_KeepLocal() +{ + testDeletionConflictTemplate(Group::KeepLocal, &TestMerge::assertDeletionLocalOnly); +} + +void TestMerge::testDeletionConflictEntry_KeepRemote() +{ + testDeletionConflictTemplate(Group::KeepRemote, &TestMerge::assertDeletionLocalOnly); +} + +void TestMerge::testDeletionConflictEntry_KeepNewer() +{ + testDeletionConflictTemplate(Group::KeepNewer, &TestMerge::assertDeletionLocalOnly); +} + +void TestMerge::testDeletionConflictEntry_Duplicate() +{ + testDeletionConflictTemplate(Group::Duplicate, &TestMerge::assertDeletionLocalOnly); +} + +/** + * Tests the KeepNewer mode concerning history. + */ +void TestMerge::testResolveConflictEntry_Synchronize() +{ + testResolveConflictTemplate(Group::Synchronize, [](Database* db, const QMap& timestamps) { + QPointer mergedRootGroup = db->rootGroup(); + QPointer mergedGroup1 = mergedRootGroup->children().at(0); + TestMerge::assertUpdateMergedEntry1(mergedGroup1->entries().at(0), timestamps); + TestMerge::assertUpdateMergedEntry2(mergedGroup1->entries().at(1), timestamps); + }); +} + +/** + * Tests the KeepExisting mode concerning history. + */ +void TestMerge::testResolveConflictEntry_KeepLocal() +{ + testResolveConflictTemplate(Group::KeepLocal, [](Database* db, const QMap& timestamps) { + QPointer mergedRootGroup = db->rootGroup(); + QPointer mergedGroup1 = mergedRootGroup->children().at(0); + TestMerge::assertUpdateMergedEntry1(mergedGroup1->entries().at(0), timestamps); + TestMerge::assertUpdateReappliedEntry2(mergedGroup1->entries().at(1), timestamps); + }); +} + +void TestMerge::testResolveConflictEntry_KeepRemote() +{ + testResolveConflictTemplate(Group::KeepRemote, [](Database* db, const QMap& timestamps) { + QPointer mergedRootGroup = db->rootGroup(); + QPointer mergedGroup1 = mergedRootGroup->children().at(0); + TestMerge::assertUpdateReappliedEntry1(mergedGroup1->entries().at(0), timestamps); + TestMerge::assertUpdateMergedEntry2(mergedGroup1->entries().at(1), timestamps); + }); +} + +void TestMerge::testResolveConflictEntry_KeepNewer() +{ + testResolveConflictTemplate(Group::KeepNewer, [](Database* db, const QMap& timestamps) { + QPointer mergedRootGroup = db->rootGroup(); + QPointer mergedGroup1 = mergedRootGroup->children().at(0); + TestMerge::assertUpdateMergedEntry1(mergedGroup1->entries().at(0), timestamps); + TestMerge::assertUpdateMergedEntry2(mergedGroup1->entries().at(1), timestamps); + }); } /** @@ -228,31 +786,31 @@ void TestMerge::testResolveConflictKeepBoth() */ void TestMerge::testMoveEntry() { - Database* dbDestination = createTestDatabase(); + QScopedPointer dbDestination(createTestDatabase()); + QScopedPointer dbSource( + createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries)); - Database* dbSource = new Database(); - dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries)); + QPointer entrySourceInitial = dbSource->rootGroup()->findEntry("entry1"); + QVERIFY(entrySourceInitial != nullptr); - Entry* entry1 = dbSource->rootGroup()->findEntry("entry1"); - QVERIFY(entry1 != nullptr); - - Group* group2 = dbSource->rootGroup()->findChildByName("group2"); - QVERIFY(group2 != nullptr); + QPointer groupSourceInitial = dbSource->rootGroup()->findChildByName("group2"); + QVERIFY(groupSourceInitial != nullptr); // Make sure the two changes have a different timestamp. - QTest::qSleep(1); - entry1->setGroup(group2); - QCOMPARE(entry1->group()->name(), QString("group2")); + m_clock->advanceSecond(1); - dbDestination->merge(dbSource); + entrySourceInitial->setGroup(groupSourceInitial); + QCOMPARE(entrySourceInitial->group()->name(), QString("group2")); - entry1 = dbDestination->rootGroup()->findEntry("entry1"); - QVERIFY(entry1 != nullptr); - QCOMPARE(entry1->group()->name(), QString("group2")); + m_clock->advanceSecond(1); + + Merger merger(dbSource.data(), dbDestination.data()); + merger.merge(); + + QPointer entryDestinationMerged = dbDestination->rootGroup()->findEntry("entry1"); + QVERIFY(entryDestinationMerged != nullptr); + QCOMPARE(entryDestinationMerged->group()->name(), QString("group2")); QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2); - - delete dbDestination; - delete dbSource; } /** @@ -262,95 +820,96 @@ void TestMerge::testMoveEntry() */ void TestMerge::testMoveEntryPreserveChanges() { - Database* dbDestination = createTestDatabase(); + QScopedPointer dbDestination(createTestDatabase()); + QScopedPointer dbSource( + createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries)); - Database* dbSource = new Database(); - dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries)); + QPointer entrySourceInitial = dbSource->rootGroup()->findEntry("entry1"); + QVERIFY(entrySourceInitial != nullptr); - Entry* entry1 = dbSource->rootGroup()->findEntry("entry1"); - QVERIFY(entry1 != nullptr); + QPointer group2Source = dbSource->rootGroup()->findChildByName("group2"); + QVERIFY(group2Source != nullptr); - Group* group2 = dbSource->rootGroup()->findChildByName("group2"); - QVERIFY(group2 != nullptr); + m_clock->advanceSecond(1); - QTest::qSleep(1); - entry1->setGroup(group2); - QCOMPARE(entry1->group()->name(), QString("group2")); + entrySourceInitial->setGroup(group2Source); + QCOMPARE(entrySourceInitial->group()->name(), QString("group2")); - entry1 = dbDestination->rootGroup()->findEntry("entry1"); - QVERIFY(entry1 != nullptr); + QPointer entryDestinationInitial = dbDestination->rootGroup()->findEntry("entry1"); + QVERIFY(entryDestinationInitial != nullptr); - QTest::qSleep(1); - entry1->beginUpdate(); - entry1->setPassword("password"); - entry1->endUpdate(); + m_clock->advanceSecond(1); - dbDestination->merge(dbSource); + entryDestinationInitial->beginUpdate(); + entryDestinationInitial->setPassword("password"); + entryDestinationInitial->endUpdate(); - entry1 = dbDestination->rootGroup()->findEntry("entry1"); - QVERIFY(entry1 != nullptr); - QCOMPARE(entry1->group()->name(), QString("group2")); + m_clock->advanceSecond(1); + + Merger merger(dbSource.data(), dbDestination.data()); + merger.merge(); + + QPointer entryDestinationMerged = dbDestination->rootGroup()->findEntry("entry1"); + QVERIFY(entryDestinationMerged != nullptr); + QCOMPARE(entryDestinationMerged->group()->name(), QString("group2")); QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2); - QCOMPARE(entry1->password(), QString("password")); - - delete dbDestination; - delete dbSource; + QCOMPARE(entryDestinationMerged->password(), QString("password")); } void TestMerge::testCreateNewGroups() { - Database* dbDestination = createTestDatabase(); + QScopedPointer dbDestination(createTestDatabase()); + QScopedPointer dbSource( + createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries)); - Database* dbSource = new Database(); - dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries)); + m_clock->advanceSecond(1); - QTest::qSleep(1); - Group* group3 = new Group(); - group3->setName("group3"); - group3->setUuid(QUuid::createUuid()); - group3->setParent(dbSource->rootGroup()); + Group* groupSourceCreated = new Group(); + groupSourceCreated->setName("group3"); + groupSourceCreated->setUuid(QUuid::createUuid()); + groupSourceCreated->setParent(dbSource->rootGroup()); - dbDestination->merge(dbSource); + m_clock->advanceSecond(1); - group3 = dbDestination->rootGroup()->findChildByName("group3"); - QVERIFY(group3 != nullptr); - QCOMPARE(group3->name(), QString("group3")); + Merger merger(dbSource.data(), dbDestination.data()); + merger.merge(); - delete dbDestination; - delete dbSource; + QPointer groupDestinationMerged = dbDestination->rootGroup()->findChildByName("group3"); + QVERIFY(groupDestinationMerged != nullptr); + QCOMPARE(groupDestinationMerged->name(), QString("group3")); } void TestMerge::testMoveEntryIntoNewGroup() { - Database* dbDestination = createTestDatabase(); + QScopedPointer dbDestination(createTestDatabase()); + QScopedPointer dbSource( + createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries)); - Database* dbSource = new Database(); - dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries)); + m_clock->advanceSecond(1); - QTest::qSleep(1); - Group* group3 = new Group(); - group3->setName("group3"); - group3->setUuid(QUuid::createUuid()); - group3->setParent(dbSource->rootGroup()); + Group* groupSourceCreated = new Group(); + groupSourceCreated->setName("group3"); + groupSourceCreated->setUuid(QUuid::createUuid()); + groupSourceCreated->setParent(dbSource->rootGroup()); - Entry* entry1 = dbSource->rootGroup()->findEntry("entry1"); - entry1->setGroup(group3); + QPointer entrySourceMoved = dbSource->rootGroup()->findEntry("entry1"); + entrySourceMoved->setGroup(groupSourceCreated); - dbDestination->merge(dbSource); + m_clock->advanceSecond(1); + + Merger merger(dbSource.data(), dbDestination.data()); + merger.merge(); QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2); - group3 = dbDestination->rootGroup()->findChildByName("group3"); - QVERIFY(group3 != nullptr); - QCOMPARE(group3->name(), QString("group3")); - QCOMPARE(group3->entries().size(), 1); + QPointer groupDestinationMerged = dbDestination->rootGroup()->findChildByName("group3"); + QVERIFY(groupDestinationMerged != nullptr); + QCOMPARE(groupDestinationMerged->name(), QString("group3")); + QCOMPARE(groupDestinationMerged->entries().size(), 1); - entry1 = dbDestination->rootGroup()->findEntry("entry1"); - QVERIFY(entry1 != nullptr); - QCOMPARE(entry1->group()->name(), QString("group3")); - - delete dbDestination; - delete dbSource; + QPointer entryDestinationMerged = dbDestination->rootGroup()->findEntry("entry1"); + QVERIFY(entryDestinationMerged != nullptr); + QCOMPARE(entryDestinationMerged->group()->name(), QString("group3")); } /** @@ -359,42 +918,50 @@ void TestMerge::testMoveEntryIntoNewGroup() */ void TestMerge::testUpdateEntryDifferentLocation() { - Database* dbDestination = createTestDatabase(); + QScopedPointer dbDestination(createTestDatabase()); + QScopedPointer dbSource( + createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries)); - Database* dbSource = new Database(); - dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries)); + Group* groupDestinationCreated = new Group(); + groupDestinationCreated->setName("group3"); + groupDestinationCreated->setUuid(QUuid::createUuid()); + groupDestinationCreated->setParent(dbDestination->rootGroup()); - Group* group3 = new Group(); - group3->setName("group3"); - group3->setUuid(QUuid::createUuid()); - group3->setParent(dbDestination->rootGroup()); + m_clock->advanceSecond(1); - Entry* entry1 = dbDestination->rootGroup()->findEntry("entry1"); - QVERIFY(entry1 != nullptr); - entry1->setGroup(group3); - QUuid uuidBeforeSyncing = entry1->uuid(); + QPointer entryDestinationMoved = dbDestination->rootGroup()->findEntry("entry1"); + QVERIFY(entryDestinationMoved != nullptr); + entryDestinationMoved->setGroup(groupDestinationCreated); + QUuid uuidBeforeSyncing = entryDestinationMoved->uuid(); + QDateTime destinationLocationChanged = entryDestinationMoved->timeInfo().locationChanged(); // 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(); + m_clock->advanceSecond(1); - dbDestination->merge(dbSource); + QPointer entrySourceMoved = dbSource->rootGroup()->findEntry("entry1"); + QVERIFY(entrySourceMoved != nullptr); + entrySourceMoved->beginUpdate(); + entrySourceMoved->setUsername("username"); + entrySourceMoved->endUpdate(); + QDateTime sourceLocationChanged = entrySourceMoved->timeInfo().locationChanged(); + + QVERIFY(destinationLocationChanged > sourceLocationChanged); + + m_clock->advanceSecond(1); + + Merger merger(dbSource.data(), dbDestination.data()); + merger.merge(); 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; + QPointer entryDestinationMerged = dbDestination->rootGroup()->findEntry("entry1"); + QVERIFY(entryDestinationMerged != nullptr); + QVERIFY(entryDestinationMerged->group() != nullptr); + QCOMPARE(entryDestinationMerged->username(), QString("username")); + QCOMPARE(entryDestinationMerged->group()->name(), QString("group3")); + QCOMPARE(uuidBeforeSyncing, entryDestinationMerged->uuid()); + // default merge strategie is KeepNewer - therefore the older location is used! + QCOMPARE(entryDestinationMerged->timeInfo().locationChanged(), sourceLocationChanged); } /** @@ -402,77 +969,90 @@ void TestMerge::testUpdateEntryDifferentLocation() */ void TestMerge::testUpdateGroup() { - Database* dbDestination = createTestDatabase(); + QScopedPointer dbDestination(createTestDatabase()); + QScopedPointer dbSource( + createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries)); - Database* dbSource = new Database(); - dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries)); + m_clock->advanceSecond(1); - QTest::qSleep(1); - - Group* group2 = dbSource->rootGroup()->findChildByName("group2"); - group2->setName("group2 renamed"); - group2->setNotes("updated notes"); + QPointer groupSourceInitial = dbSource->rootGroup()->findChildByName("group2"); + groupSourceInitial->setName("group2 renamed"); + groupSourceInitial->setNotes("updated notes"); QUuid customIconId = QUuid::createUuid(); QImage customIcon; dbSource->metadata()->addCustomIcon(customIconId, customIcon); - group2->setIcon(customIconId); + groupSourceInitial->setIcon(customIconId); - Entry* entry1 = dbSource->rootGroup()->findEntry("entry1"); - QVERIFY(entry1 != nullptr); - entry1->setGroup(group2); - entry1->setTitle("entry1 renamed"); - QUuid uuidBeforeSyncing = entry1->uuid(); + QPointer entrySourceInitial = dbSource->rootGroup()->findEntry("entry1"); + QVERIFY(entrySourceInitial != nullptr); + entrySourceInitial->setGroup(groupSourceInitial); + entrySourceInitial->setTitle("entry1 renamed"); + QUuid uuidBeforeSyncing = entrySourceInitial->uuid(); - dbDestination->merge(dbSource); + m_clock->advanceSecond(1); + + Merger merger(dbSource.data(), dbDestination.data()); + merger.merge(); QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2); - entry1 = dbDestination->rootGroup()->findEntry("entry1 renamed"); - QVERIFY(entry1 != nullptr); - QVERIFY(entry1->group() != nullptr); - QCOMPARE(entry1->group()->name(), QString("group2 renamed")); - QCOMPARE(uuidBeforeSyncing, entry1->uuid()); + QPointer entryDestinationMerged = dbDestination->rootGroup()->findEntry("entry1 renamed"); + QVERIFY(entryDestinationMerged != nullptr); + QVERIFY(entryDestinationMerged->group() != nullptr); + QCOMPARE(entryDestinationMerged->group()->name(), QString("group2 renamed")); + QCOMPARE(uuidBeforeSyncing, entryDestinationMerged->uuid()); - group2 = dbDestination->rootGroup()->findChildByName("group2 renamed"); - QCOMPARE(group2->notes(), QString("updated notes")); - QCOMPARE(group2->iconUuid(), customIconId); - - delete dbDestination; - delete dbSource; + QPointer groupMerged = dbDestination->rootGroup()->findChildByName("group2 renamed"); + QCOMPARE(groupMerged->notes(), QString("updated notes")); + QCOMPARE(groupMerged->iconUuid(), customIconId); } void TestMerge::testUpdateGroupLocation() { - Database* dbDestination = createTestDatabase(); - Group* group3 = new Group(); + QScopedPointer dbDestination(createTestDatabase()); + Group* group3DestinationCreated = new Group(); QUuid group3Uuid = QUuid::createUuid(); - group3->setUuid(group3Uuid); - group3->setName("group3"); - group3->setParent(dbDestination->rootGroup()->findChildByName("group1")); + group3DestinationCreated->setUuid(group3Uuid); + group3DestinationCreated->setName("group3"); + group3DestinationCreated->setParent(dbDestination->rootGroup()->findChildByName("group1")); - Database* dbSource = new Database(); - dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries)); + QScopedPointer dbSource( + createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries)); // Sanity check - group3 = dbSource->rootGroup()->findChildByUuid(group3Uuid); - QVERIFY(group3 != nullptr); + QPointer group3SourceInitial = dbSource->rootGroup()->findGroupByUuid(group3Uuid); + QVERIFY(group3DestinationCreated != nullptr); - QTest::qSleep(1); + QDateTime initialLocationChanged = group3SourceInitial->timeInfo().locationChanged(); - group3->setParent(dbSource->rootGroup()->findChildByName("group2")); + m_clock->advanceSecond(1); - dbDestination->merge(dbSource); - group3 = dbDestination->rootGroup()->findChildByUuid(group3Uuid); - QVERIFY(group3 != nullptr); - QCOMPARE(group3->parent(), dbDestination->rootGroup()->findChildByName("group2")); + QPointer group3SourceMoved = dbSource->rootGroup()->findGroupByUuid(group3Uuid); + QVERIFY(group3SourceMoved != nullptr); + group3SourceMoved->setParent(dbSource->rootGroup()->findChildByName("group2")); - dbDestination->merge(dbSource); - group3 = dbDestination->rootGroup()->findChildByUuid(group3Uuid); - QVERIFY(group3 != nullptr); - QCOMPARE(group3->parent(), dbDestination->rootGroup()->findChildByName("group2")); + QDateTime movedLocaltionChanged = group3SourceMoved->timeInfo().locationChanged(); + QVERIFY(initialLocationChanged < movedLocaltionChanged); - delete dbDestination; - delete dbSource; + m_clock->advanceSecond(1); + + Merger merger1(dbSource.data(), dbDestination.data()); + merger1.merge(); + + QPointer group3DestinationMerged1 = dbDestination->rootGroup()->findGroupByUuid(group3Uuid); + QVERIFY(group3DestinationMerged1 != nullptr); + QCOMPARE(group3DestinationMerged1->parent(), dbDestination->rootGroup()->findChildByName("group2")); + QCOMPARE(group3DestinationMerged1->timeInfo().locationChanged(), movedLocaltionChanged); + + m_clock->advanceSecond(1); + + Merger merger2(dbSource.data(), dbDestination.data()); + merger2.merge(); + + QPointer group3DestinationMerged2 = dbDestination->rootGroup()->findGroupByUuid(group3Uuid); + QVERIFY(group3DestinationMerged2 != nullptr); + QCOMPARE(group3DestinationMerged2->parent(), dbDestination->rootGroup()->findChildByName("group2")); + QCOMPARE(group3DestinationMerged1->timeInfo().locationChanged(), movedLocaltionChanged); } /** @@ -482,22 +1062,25 @@ void TestMerge::testUpdateGroupLocation() */ void TestMerge::testMergeAndSync() { - Database* dbDestination = new Database(); - Database* dbSource = createTestDatabase(); + QScopedPointer dbDestination(new Database()); + QScopedPointer dbSource(createTestDatabase()); QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 0); - dbDestination->merge(dbSource); + m_clock->advanceSecond(1); + + Merger merger1(dbSource.data(), dbDestination.data()); + merger1.merge(); QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2); - dbDestination->merge(dbSource); + m_clock->advanceSecond(1); + + Merger merger2(dbSource.data(), dbDestination.data()); + merger2.merge(); // Still only 2 entries, since now we detect which are already present. QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2); - - delete dbDestination; - delete dbSource; } /** @@ -505,8 +1088,10 @@ void TestMerge::testMergeAndSync() */ void TestMerge::testMergeCustomIcons() { - Database* dbDestination = new Database(); - Database* dbSource = createTestDatabase(); + QScopedPointer dbDestination(new Database()); + QScopedPointer dbSource(createTestDatabase()); + + m_clock->advanceSecond(1); QUuid customIconId = QUuid::createUuid(); QImage customIcon; @@ -515,12 +1100,222 @@ void TestMerge::testMergeCustomIcons() // Sanity check. QVERIFY(dbSource->metadata()->containsCustomIcon(customIconId)); - dbDestination->merge(dbSource); + m_clock->advanceSecond(1); + + Merger merger(dbSource.data(), dbDestination.data()); + merger.merge(); QVERIFY(dbDestination->metadata()->containsCustomIcon(customIconId)); +} - delete dbDestination; - delete dbSource; +void TestMerge::testMetadata() +{ + QSKIP("Sophisticated merging for Metadata not implemented"); + // TODO HNH: I think a merge of recycle bins would be nice since duplicating them + // is not realy a good solution - the one to use as final recycle bin + // is determined by the merge method - if only one has a bin, this one + // will be used - exception is the target has no recycle bin activated +} + +void TestMerge::testDeletedEntry() +{ + QScopedPointer dbDestination(createTestDatabase()); + QScopedPointer dbSource( + createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries)); + + m_clock->advanceSecond(1); + + QPointer entry1SourceInitial = dbSource->rootGroup()->findEntry("entry1"); + QVERIFY(entry1SourceInitial != nullptr); + QUuid entry1Uuid = entry1SourceInitial->uuid(); + delete entry1SourceInitial; + QVERIFY(dbSource->containsDeletedObject(entry1Uuid)); + + m_clock->advanceSecond(1); + + QPointer entry2DestinationInitial = dbDestination->rootGroup()->findEntry("entry2"); + QVERIFY(entry2DestinationInitial != nullptr); + QUuid entry2Uuid = entry2DestinationInitial->uuid(); + delete entry2DestinationInitial; + QVERIFY(dbDestination->containsDeletedObject(entry2Uuid)); + + m_clock->advanceSecond(1); + + Merger merger(dbSource.data(), dbDestination.data()); + merger.merge(); + + QPointer entry1DestinationMerged = dbDestination->rootGroup()->findEntry("entry1"); + QVERIFY(entry1DestinationMerged); + QVERIFY(!dbDestination->containsDeletedObject(entry1Uuid)); + QPointer entry2DestinationMerged = dbDestination->rootGroup()->findEntry("entry2"); + QVERIFY(entry2DestinationMerged); + // Uuid in db and deletedObjects is intended according to KeePass #1752 + QVERIFY(dbDestination->containsDeletedObject(entry2Uuid)); + + QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2); +} + +void TestMerge::testDeletedGroup() +{ + QScopedPointer dbDestination(createTestDatabase()); + QScopedPointer dbSource( + createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries)); + + m_clock->advanceSecond(1); + + QPointer group2DestinationInitial = dbDestination->rootGroup()->findChildByName("group2"); + QVERIFY(group2DestinationInitial != nullptr); + Entry* entry3DestinationCreated = new Entry(); + entry3DestinationCreated->beginUpdate(); + entry3DestinationCreated->setUuid(QUuid::createUuid()); + entry3DestinationCreated->setGroup(group2DestinationInitial); + entry3DestinationCreated->setTitle("entry3"); + entry3DestinationCreated->endUpdate(); + + m_clock->advanceSecond(1); + + QPointer group1SourceInitial = dbSource->rootGroup()->findChildByName("group1"); + QVERIFY(group1SourceInitial != nullptr); + QPointer entry1SourceInitial = dbSource->rootGroup()->findEntry("entry1"); + QVERIFY(entry1SourceInitial != nullptr); + QPointer entry2SourceInitial = dbSource->rootGroup()->findEntry("entry2"); + QVERIFY(entry2SourceInitial != nullptr); + QUuid group1Uuid = group1SourceInitial->uuid(); + QUuid entry1Uuid = entry1SourceInitial->uuid(); + QUuid entry2Uuid = entry2SourceInitial->uuid(); + delete group1SourceInitial; + QVERIFY(dbSource->containsDeletedObject(group1Uuid)); + QVERIFY(dbSource->containsDeletedObject(entry1Uuid)); + QVERIFY(dbSource->containsDeletedObject(entry2Uuid)); + + m_clock->advanceSecond(1); + + QPointer group2SourceInitial = dbSource->rootGroup()->findChildByName("group2"); + QVERIFY(group2SourceInitial != nullptr); + QUuid group2Uuid = group2SourceInitial->uuid(); + delete group2SourceInitial; + QVERIFY(dbSource->containsDeletedObject(group2Uuid)); + + m_clock->advanceSecond(1); + + Merger merger(dbSource.data(), dbDestination.data()); + merger.merge(); + + QVERIFY(!dbDestination->containsDeletedObject(group1Uuid)); + QVERIFY(!dbDestination->containsDeletedObject(entry1Uuid)); + QVERIFY(!dbDestination->containsDeletedObject(entry2Uuid)); + QVERIFY(!dbDestination->containsDeletedObject(group2Uuid)); + + QPointer entry1DestinationMerged = dbDestination->rootGroup()->findEntry("entry1"); + QVERIFY(entry1DestinationMerged); + QPointer entry2DestinationMerged = dbDestination->rootGroup()->findEntry("entry2"); + QVERIFY(entry2DestinationMerged); + QPointer entry3DestinationMerged = dbDestination->rootGroup()->findEntry("entry3"); + QVERIFY(entry3DestinationMerged); + QPointer group1DestinationMerged = dbDestination->rootGroup()->findChildByName("group1"); + QVERIFY(group1DestinationMerged); + QPointer group2DestinationMerged = dbDestination->rootGroup()->findChildByName("group2"); + QVERIFY(group2DestinationMerged); + + QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 3); +} + +void TestMerge::testDeletedRevertedEntry() +{ + QScopedPointer dbDestination(createTestDatabase()); + QScopedPointer dbSource( + createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries)); + + m_clock->advanceSecond(1); + + QPointer entry1DestinationInitial = dbDestination->rootGroup()->findEntry("entry1"); + QVERIFY(entry1DestinationInitial != nullptr); + QUuid entry1Uuid = entry1DestinationInitial->uuid(); + delete entry1DestinationInitial; + QVERIFY(dbDestination->containsDeletedObject(entry1Uuid)); + + m_clock->advanceSecond(1); + + QPointer entry2SourceInitial = dbSource->rootGroup()->findEntry("entry2"); + QVERIFY(entry2SourceInitial != nullptr); + QUuid entry2Uuid = entry2SourceInitial->uuid(); + delete entry2SourceInitial; + QVERIFY(dbSource->containsDeletedObject(entry2Uuid)); + + m_clock->advanceSecond(1); + + QPointer entry1SourceInitial = dbSource->rootGroup()->findEntry("entry1"); + QVERIFY(entry1SourceInitial != nullptr); + entry1SourceInitial->setNotes("Updated"); + + QPointer entry2DestinationInitial = dbDestination->rootGroup()->findEntry("entry2"); + QVERIFY(entry2DestinationInitial != nullptr); + entry2DestinationInitial->setNotes("Updated"); + + Merger merger(dbSource.data(), dbDestination.data()); + merger.merge(); + + // Uuid in db and deletedObjects is intended according to KeePass #1752 + QVERIFY(dbDestination->containsDeletedObject(entry1Uuid)); + QVERIFY(!dbDestination->containsDeletedObject(entry2Uuid)); + + QPointer entry1DestinationMerged = dbDestination->rootGroup()->findEntry("entry1"); + QVERIFY(entry1DestinationMerged); + QVERIFY(entry1DestinationMerged->notes() == "Updated"); + QPointer entry2DestinationMerged = dbDestination->rootGroup()->findEntry("entry2"); + QVERIFY(entry2DestinationMerged); + QVERIFY(entry2DestinationMerged->notes() == "Updated"); +} + +void TestMerge::testDeletedRevertedGroup() +{ + QScopedPointer dbDestination(createTestDatabase()); + QScopedPointer dbSource( + createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries)); + + m_clock->advanceSecond(1); + + QPointer group2SourceInitial = dbSource->rootGroup()->findChildByName("group2"); + QVERIFY(group2SourceInitial); + QUuid group2Uuid = group2SourceInitial->uuid(); + delete group2SourceInitial; + QVERIFY(dbSource->containsDeletedObject(group2Uuid)); + + m_clock->advanceSecond(1); + + QPointer group1DestinationInitial = dbDestination->rootGroup()->findChildByName("group1"); + QVERIFY(group1DestinationInitial); + QUuid group1Uuid = group1DestinationInitial->uuid(); + delete group1DestinationInitial; + QVERIFY(dbDestination->containsDeletedObject(group1Uuid)); + + m_clock->advanceSecond(1); + + QPointer group1SourceInitial = dbSource->rootGroup()->findChildByName("group1"); + QVERIFY(group1SourceInitial); + group1SourceInitial->setNotes("Updated"); + + m_clock->advanceSecond(1); + + QPointer group2DestinationInitial = dbDestination->rootGroup()->findChildByName("group2"); + QVERIFY(group2DestinationInitial); + group2DestinationInitial->setNotes("Updated"); + + m_clock->advanceSecond(1); + + Merger merger(dbSource.data(), dbDestination.data()); + merger.merge(); + + // Uuid in db and deletedObjects is intended according to KeePass #1752 + QVERIFY(dbDestination->containsDeletedObject(group1Uuid)); + QVERIFY(!dbDestination->containsDeletedObject(group2Uuid)); + + QPointer group1DestinationMerged = dbDestination->rootGroup()->findChildByName("group1"); + QVERIFY(group1DestinationMerged); + QVERIFY(group1DestinationMerged->notes() == "Updated"); + QPointer group2DestinationMerged = dbDestination->rootGroup()->findChildByName("group2"); + QVERIFY(group2DestinationMerged); + QVERIFY(group2DestinationMerged->notes() == "Updated"); } /** @@ -530,33 +1325,34 @@ void TestMerge::testMergeCustomIcons() */ void TestMerge::testResolveGroupConflictOlder() { - Database* dbDestination = createTestDatabase(); - - Database* dbSource = new Database(); - dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries)); + QScopedPointer dbDestination(createTestDatabase()); + QScopedPointer dbSource( + createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries)); // sanity check - Group* group1 = dbSource->rootGroup()->findChildByName("group1"); - QVERIFY(group1 != nullptr); + QPointer groupSourceInitial = dbSource->rootGroup()->findChildByName("group1"); + QVERIFY(groupSourceInitial != nullptr); // Make sure the two changes have a different timestamp. - QTest::qSleep(1); - group1->setName("group1 updated in source"); + m_clock->advanceSecond(1); + + groupSourceInitial->setName("group1 updated in source"); // Make sure the two changes have a different timestamp. - QTest::qSleep(1); + m_clock->advanceSecond(1); - group1 = dbDestination->rootGroup()->findChildByName("group1"); - group1->setName("group1 updated in destination"); + QPointer groupDestinationUpdated = dbDestination->rootGroup()->findChildByName("group1"); + groupDestinationUpdated->setName("group1 updated in destination"); - dbDestination->merge(dbSource); + m_clock->advanceSecond(1); + + Merger merger(dbSource.data(), dbDestination.data()); + merger.merge(); // sanity check - group1 = dbDestination->rootGroup()->findChildByName("group1 updated in destination"); - QVERIFY(group1 != nullptr); - - delete dbDestination; - delete dbSource; + QPointer groupDestinationMerged = + dbDestination->rootGroup()->findChildByName("group1 updated in destination"); + QVERIFY(groupDestinationMerged != nullptr); } Database* TestMerge::createTestDatabase() @@ -572,19 +1368,21 @@ Database* TestMerge::createTestDatabase() group2->setUuid(QUuid::createUuid()); Entry* entry1 = new Entry(); + entry1->setUuid(QUuid::createUuid()); Entry* entry2 = new Entry(); + entry2->setUuid(QUuid::createUuid()); + + m_clock->advanceYear(1); // Give Entry 1 a history entry1->beginUpdate(); entry1->setGroup(group1); - entry1->setUuid(QUuid::createUuid()); entry1->setTitle("entry1"); entry1->endUpdate(); // Give Entry 2 a history entry2->beginUpdate(); entry2->setGroup(group1); - entry2->setUuid(QUuid::createUuid()); entry2->setTitle("entry2"); entry2->endUpdate(); @@ -593,3 +1391,12 @@ Database* TestMerge::createTestDatabase() return db; } + +Database* TestMerge::createTestDatabaseStructureClone(Database* source, int entryFlags, int groupFlags) +{ + Database* db = new Database(); + // the old root group is deleted by QObject::parent relationship + db->setRootGroup(source->rootGroup()->clone(static_cast(entryFlags), + static_cast(groupFlags))); + return db; +} diff --git a/tests/TestMerge.h b/tests/TestMerge.h index 3588cfd53..15c18e43b 100644 --- a/tests/TestMerge.h +++ b/tests/TestMerge.h @@ -19,20 +19,33 @@ #define KEEPASSX_TESTMERGE_H #include "core/Database.h" +#include +#include #include +#include class TestMerge : public QObject { Q_OBJECT - private slots: void initTestCase(); + void init(); + void cleanup(); void testMergeIntoNew(); void testMergeNoChanges(); void testResolveConflictNewer(); - void testResolveConflictOlder(); + void testResolveConflictExisting(); void testResolveGroupConflictOlder(); - void testResolveConflictKeepBoth(); + void testResolveConflictDuplicate(); + void testResolveConflictEntry_Synchronize(); + void testResolveConflictEntry_KeepLocal(); + void testResolveConflictEntry_KeepRemote(); + void testResolveConflictEntry_KeepNewer(); + void testDeletionConflictEntry_Duplicate(); + void testDeletionConflictEntry_Synchronized(); + void testDeletionConflictEntry_KeepLocal(); + void testDeletionConflictEntry_KeepRemote(); + void testDeletionConflictEntry_KeepNewer(); void testMoveEntry(); void testMoveEntryPreserveChanges(); void testMoveEntryIntoNewGroup(); @@ -42,9 +55,24 @@ private slots: void testUpdateGroupLocation(); void testMergeAndSync(); void testMergeCustomIcons(); + void testMetadata(); + void testDeletedEntry(); + void testDeletedGroup(); + void testDeletedRevertedEntry(); + void testDeletedRevertedGroup(); private: Database* createTestDatabase(); + Database* createTestDatabaseStructureClone(Database* source, int entryFlags, int groupFlags); + void testResolveConflictTemplate(int mergeMode, std::function&)> verification); + void testDeletionConflictTemplate(int mergeMode, std::function&)> verification); + static void assertDeletionNewerOnly(Database *db, const QMap &identifiers); + static void assertDeletionLocalOnly(Database *db, const QMap &identifiers); + static void assertUpdateMergedEntry1(Entry *entry, const QMap ×tamps); + static void assertUpdateReappliedEntry2(Entry *entry, const QMap ×tamps); + static void assertUpdateReappliedEntry1(Entry *entry, const QMap ×tamps); + static void assertUpdateMergedEntry2(Entry *entry, const QMap ×tamps); + }; #endif // KEEPASSX_TESTMERGE_H diff --git a/tests/TestModified.cpp b/tests/TestModified.cpp index 6e033f25e..b1ad09443 100644 --- a/tests/TestModified.cpp +++ b/tests/TestModified.cpp @@ -16,10 +16,12 @@ */ #include "TestModified.h" +#include "stub/TestClock.h" #include #include +#include "core/Clock.h" #include "core/Database.h" #include "core/Group.h" #include "core/Metadata.h" @@ -27,11 +29,29 @@ QTEST_GUILESS_MAIN(TestModified) +namespace +{ + TestClock* m_clock = nullptr; +} + void TestModified::initTestCase() { QVERIFY(Crypto::init()); } +void TestModified::init() +{ + Q_ASSERT(m_clock == nullptr); + m_clock = new TestClock(2010, 5, 5, 10, 30, 10); + TestClock::setup(m_clock); +} + +void TestModified::cleanup() +{ + TestClock::teardown(); + m_clock = nullptr; +} + void TestModified::testSignals() { int spyCount = 0; @@ -230,7 +250,7 @@ void TestModified::testEntrySets() entry->setExpires(entry->timeInfo().expires()); QCOMPARE(spyModified.count(), spyCount); - entry->setExpiryTime(QDateTime::currentDateTimeUtc().addYears(1)); + entry->setExpiryTime(Clock::currentDateTimeUtc().addYears(1)); QCOMPARE(spyModified.count(), ++spyCount); entry->setExpiryTime(entry->timeInfo().expiryTime()); QCOMPARE(spyModified.count(), spyCount); @@ -300,7 +320,7 @@ void TestModified::testHistoryItems() QCOMPARE(entry->historyItems().size(), historyItemsSize); QDateTime modified = entry->timeInfo().lastModificationTime(); - QTest::qSleep(10); + m_clock->advanceSecond(10); entry->beginUpdate(); entry->setTitle("b"); entry->endUpdate(); diff --git a/tests/TestModified.h b/tests/TestModified.h index 581562fa5..5f7efa4d4 100644 --- a/tests/TestModified.h +++ b/tests/TestModified.h @@ -26,6 +26,8 @@ class TestModified : public QObject private slots: void initTestCase(); + void init(); + void cleanup(); void testSignals(); void testGroupSets(); void testEntrySets(); diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 731eadcaf..e3671567c 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -92,7 +92,6 @@ void TestGui::initTestCase() Tools::wait(50); // Load the NewDatabase.kdbx file into temporary storage - QByteArray tmpData; QFile sourceDbFile(QString(KEEPASSX_TEST_DATA_DIR).append("/NewDatabase.kdbx")); QVERIFY(sourceDbFile.open(QIODevice::ReadOnly)); QVERIFY(Tools::readAllFromDevice(&sourceDbFile, m_dbData)); @@ -292,17 +291,17 @@ void TestGui::testAutoreloadDatabase() config()->set("AutoReloadOnChange", false); // Load the MergeDatabase.kdbx file into temporary storage - QByteArray tmpData; + QByteArray unmodifiedMergeDatabase; QFile mergeDbFile(QString(KEEPASSX_TEST_DATA_DIR).append("/MergeDatabase.kdbx")); QVERIFY(mergeDbFile.open(QIODevice::ReadOnly)); - QVERIFY(Tools::readAllFromDevice(&mergeDbFile, tmpData)); + QVERIFY(Tools::readAllFromDevice(&mergeDbFile, unmodifiedMergeDatabase)); mergeDbFile.close(); // Test accepting new file in autoreload MessageBox::setNextAnswer(QMessageBox::Yes); // Overwrite the current database with the temp data QVERIFY(m_dbFile.open()); - QVERIFY(m_dbFile.write(tmpData, static_cast(tmpData.size()))); + QVERIFY(m_dbFile.write(unmodifiedMergeDatabase, static_cast(unmodifiedMergeDatabase.size()))); m_dbFile.close(); Tools::wait(1500); @@ -320,7 +319,7 @@ void TestGui::testAutoreloadDatabase() MessageBox::setNextAnswer(QMessageBox::No); // Overwrite the current temp database with a new file m_dbFile.open(); - QVERIFY(m_dbFile.write(tmpData, static_cast(tmpData.size()))); + QVERIFY(m_dbFile.write(unmodifiedMergeDatabase, static_cast(unmodifiedMergeDatabase.size()))); m_dbFile.close(); Tools::wait(1500); @@ -337,7 +336,6 @@ void TestGui::testAutoreloadDatabase() // Test accepting a merge of edits into autoreload // Turn on autoload so we only get one messagebox (for the merge) config()->set("AutoReloadOnChange", true); - // Modify some entries testEditEntry(); @@ -345,7 +343,7 @@ void TestGui::testAutoreloadDatabase() MessageBox::setNextAnswer(QMessageBox::Yes); // Overwrite the current database with the temp data QVERIFY(m_dbFile.open()); - QVERIFY(m_dbFile.write(tmpData, static_cast(tmpData.size()))); + QVERIFY(m_dbFile.write(unmodifiedMergeDatabase, static_cast(unmodifiedMergeDatabase.size()))); m_dbFile.close(); Tools::wait(1500); diff --git a/tests/stub/TestClock.cpp b/tests/stub/TestClock.cpp new file mode 100644 index 000000000..d3222febd --- /dev/null +++ b/tests/stub/TestClock.cpp @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2018 KeePassXC Team + * + * 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 . + */ + +#include "TestClock.h" + +TestClock::TestClock(int year, int month, int day, int hour, int min, int second) + : Clock() + , m_utcCurrent(datetimeUtc(year, month, day, hour, min, second)) +{ +} + +TestClock::TestClock(QDateTime utcBase) + : Clock() + , m_utcCurrent(utcBase) +{ +} + +const QDateTime& TestClock::advanceSecond(int seconds) +{ + m_utcCurrent = m_utcCurrent.addSecs(seconds); + return m_utcCurrent; +} + +const QDateTime& TestClock::advanceMinute(int minutes) +{ + m_utcCurrent = m_utcCurrent.addSecs(minutes * 60); + return m_utcCurrent; +} + +const QDateTime& TestClock::advanceHour(int hours) +{ + m_utcCurrent = m_utcCurrent.addSecs(hours * 60 * 60); + return m_utcCurrent; +} + +const QDateTime& TestClock::advanceDay(int days) +{ + m_utcCurrent = m_utcCurrent.addDays(days); + return m_utcCurrent; +} + +const QDateTime& TestClock::advanceMonth(int months) +{ + m_utcCurrent = m_utcCurrent.addMonths(months); + return m_utcCurrent; +} + +const QDateTime& TestClock::advanceYear(int years) +{ + m_utcCurrent = m_utcCurrent.addYears(years); + return m_utcCurrent; +} + +void TestClock::setup(Clock* clock) +{ + Clock::setInstance(clock); +} + +void TestClock::teardown() +{ + Clock::resetInstance(); +} + +QDateTime TestClock::currentDateTimeUtcImpl() const +{ + return m_utcCurrent; +} + +QDateTime TestClock::currentDateTimeImpl() const +{ + return m_utcCurrent.toLocalTime(); +} diff --git a/tests/stub/TestClock.h b/tests/stub/TestClock.h new file mode 100644 index 000000000..02405edcb --- /dev/null +++ b/tests/stub/TestClock.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2018 KeePassXC Team + * + * 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 . + */ + +#ifndef KEEPASSXC_TESTCLOCK_H +#define KEEPASSXC_TESTCLOCK_H + +#include "core/Clock.h" + +#include + +class TestClock : public Clock +{ +public: + TestClock(int year, int month, int day, int hour, int min, int second); + + TestClock(QDateTime utcBase = QDateTime::currentDateTimeUtc()); + + const QDateTime& advanceSecond(int seconds); + const QDateTime& advanceMinute(int minutes); + const QDateTime& advanceHour(int hours); + const QDateTime& advanceDay(int days); + const QDateTime& advanceMonth(int months); + const QDateTime& advanceYear(int years); + + static void setup(Clock* clock); + static void teardown(); + +protected: + QDateTime currentDateTimeUtcImpl() const; + QDateTime currentDateTimeImpl() const; + +private: + QDateTime m_utcCurrent; +}; + +#endif // KEEPASSXC_TESTCLOCK_H