Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/keepassxreboot/keepassxc.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/CMakeLists.txt3
-rw-r--r--src/autotype/AutoType.cpp7
-rw-r--r--src/browser/BrowserService.cpp6
-rw-r--r--src/cli/Merge.cpp4
-rw-r--r--src/core/AutoTypeAssociations.cpp18
-rw-r--r--src/core/AutoTypeAssociations.h3
-rw-r--r--src/core/Clock.cpp109
-rw-r--r--src/core/Clock.h58
-rw-r--r--src/core/Compare.cpp38
-rw-r--r--src/core/Compare.h90
-rw-r--r--src/core/Database.cpp62
-rw-r--r--src/core/Database.h10
-rw-r--r--src/core/Entry.cpp167
-rw-r--r--src/core/Entry.h15
-rw-r--r--src/core/Group.cpp240
-rw-r--r--src/core/Group.h32
-rw-r--r--src/core/Merger.cpp613
-rw-r--r--src/core/Merger.h72
-rw-r--r--src/core/Metadata.cpp5
-rw-r--r--src/core/TimeInfo.cpp39
-rw-r--r--src/core/TimeInfo.h7
-rw-r--r--src/core/Tools.h1
-rw-r--r--src/format/KdbxXmlReader.cpp5
-rw-r--r--src/gui/DatabaseWidget.cpp11
-rw-r--r--src/gui/TotpDialog.cpp5
-rw-r--r--src/gui/csvImport/CsvImportWidget.cpp6
-rw-r--r--src/gui/dbsettings/DatabaseSettingsWidgetGeneral.cpp3
-rw-r--r--src/gui/entry/EditEntryWidget.cpp5
-rw-r--r--src/gui/group/EditGroupWidget.h11
-rw-r--r--src/totp/totp.cpp5
-rw-r--r--tests/CMakeLists.txt12
-rw-r--r--tests/TestEntry.cpp69
-rw-r--r--tests/TestGlobal.h10
-rw-r--r--tests/TestGroup.cpp25
-rw-r--r--tests/TestGroup.h2
-rw-r--r--tests/TestKeePass2Format.cpp41
-rw-r--r--tests/TestMerge.cpp1349
-rw-r--r--tests/TestMerge.h34
-rw-r--r--tests/TestModified.cpp24
-rw-r--r--tests/TestModified.h2
-rw-r--r--tests/gui/TestGui.cpp12
-rw-r--r--tests/stub/TestClock.cpp86
-rw-r--r--tests/stub/TestClock.h50
43 files changed, 2779 insertions, 587 deletions
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 <QTextStream>
#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<AutoTypeAssociations::Association> 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 <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+#include "Clock.h"
+
+QSharedPointer<Clock> Clock::m_instance = QSharedPointer<Clock>();
+
+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>(clock);
+}
+
+const Clock& Clock::instance()
+{
+ if (!m_instance) {
+ m_instance = QSharedPointer<Clock>(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 <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef KEEPASSXC_CLOCK_H
+#define KEEPASSXC_CLOCK_H
+
+#include <QDateTime>
+#include <QSharedPointer>
+
+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<Clock> 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 <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+#include "Compare.h"
+
+#include <QColor>
+
+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 <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef KEEPASSXC_COMPARE_H
+#define KEEPASSXC_COMPARE_H
+
+#include <QDateTime>
+
+#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 <typename Type> inline short compareGeneric(const Type& lhs, const Type& rhs, CompareItemOptions)
+{
+ if (lhs != rhs) {
+ return lhs < rhs ? -1 : +1;
+ }
+ return 0;
+}
+
+template <typename Type>
+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 <typename Type>
+inline short compare(bool enabled, const Type& lhs, const Type& rhs, CompareItemOptions options = CompareItemDefault)
+{
+ if (!enabled) {
+ return 0;
+ }
+ return compare(lhs, rhs, options);
+}
+
+template <typename Type>
+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 <QXmlStreamReader>
#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<QUuid, Database*> 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<DeletedObject> Database::deletedObjects()
return m_deletedObjects;
}
+const QList<DeletedObject>& 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<DeletedObject>& 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<const CompositeKey> 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<con
KeePass2Reader reader;
Database* db = reader.readDatabase(&dbFile, key);
-
if (reader.hasError()) {
qCritical("Error while parsing the database: %s", qPrintable(reader.errorString()));
return nullptr;
@@ -600,7 +625,6 @@ QString Database::writeDatabase(QIODevice* device)
* @param filePath Path to the file to backup
* @return
*/
-
bool Database::backupDatabase(QString filePath)
{
QString backupFilePath = filePath;
diff --git a/src/core/Database.h b/src/core/Database.h
index 912f95073..a5ae3effa 100644
--- a/src/core/Database.h
+++ b/src/core/Database.h
@@ -37,6 +37,10 @@ struct DeletedObject
{
QUuid uuid;
QDateTime deletionTime;
+ bool operator==(const DeletedObject& other) const
+ {
+ return uuid == other.uuid && deletionTime == other.deletionTime;
+ }
};
Q_DECLARE_TYPEINFO(DeletedObject, Q_MOVABLE_TYPE);
@@ -88,8 +92,12 @@ public:
Entry* resolveEntry(const QString& text, EntryReferenceType referenceType);
Group* resolveGroup(const QUuid& uuid);
QList<DeletedObject> deletedObjects();
+ const QList<DeletedObject>& 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<DeletedObject>& 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 <class T> 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<Entry*>& 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<Totp::Settings> 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<Entry*>& 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 <class T> bool set(T& property, const T& value);
QUuid m_uuid;
@@ -238,8 +247,8 @@ private:
QPointer<EntryAttachments> m_attachments;
QPointer<AutoTypeAssociations> m_autoTypeAssociations;
QPointer<CustomData> m_customData;
+ QList<Entry*> m_history; // Items sorted from oldest to newest
- QList<Entry*> m_history;
Entry* m_tmpHistoryItem;
bool m_modifiedSinceBegin;
QPointer<Group> 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<Entry*> 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 <class P, class V> 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<QUuid> Group::customIconsRecursive() const
return result;
}
-void Group::merge(const Group* other)
-{
-
- Group* rootGroup = this;
- while (rootGroup->parentGroup()) {
- rootGroup = rootGroup->parentGroup();
- }
-
- // merge entries
- const QList<Entry*> 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<Group*> 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 <class P, class V> 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 <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "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<Entry*> 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<Group*> 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<DeletedObject> 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<DeletedObject> 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<QDateTime, Entry*> 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<DeletedObject> deletions;
+ QMap<QUuid, DeletedObject> mergedDeletions;
+ QList<Entry*> entries;
+ QList<Group*> 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 <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef KEEPASSXC_MERGER_H
+#define KEEPASSXC_MERGER_H
+
+#include "core/Group.h"
+#include <QObject>
+#include <QPointer>
+
+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<const Database> m_sourceDb;
+ QPointer<Database> m_targetDb;
+ QPointer<const Group> m_sourceRootGroup;
+ QPointer<Group> m_targetRootGroup;
+ QPointer<const Group> m_sourceGroup;
+ QPointer<Group> 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 <QtCore/QCryptographicHash>
+#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 <class P, class V> 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 <QDateTime>
+#include <QFlag>
+
+#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 <QDateTime>
#include <QObject>
#include <QString>
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 <b>%n</b> 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<int>(static_cast<double>(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 <QFileInfo>
#include <QSpacerItem>
+#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 <QButtonGroup>
#include <QColorDialog>
#include <QDesktopServices>
#include <QEvent>
@@ -32,9 +33,9 @@
#include <QStackedLayout>
#include <QStandardPaths>
#include <QTemporaryFile>
-#include <QButtonGroup>
#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<TimeDelta>();
- 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<Ui::EditGroupWidgetMain> m_mainUi;
- QWidget* const m_editGroupWidgetMain;
- EditWidgetIcons* const m_editGroupWidgetIcons;
- EditWidgetProperties* const m_editWidgetProperties;
- Group* m_group;
- Database* m_database;
+ QPointer<QWidget> m_editGroupWidgetMain;
+ QPointer<EditWidgetIcons> m_editGroupWidgetIcons;
+ QPointer<EditWidgetProperties> m_editWidgetProperties;
+
+ QPointer<Group> m_group;
+ QPointer<Database> 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 <QCryptographicHash>
-#include <QDateTime>
#include <QMessageAuthenticationCode>
#include <QRegExp>
#include <QUrl>
@@ -133,8 +133,7 @@ QString Totp::generateTotp(const QSharedPointer<Totp::Settings> settings, const
quint64 current;
if (time == 0) {
- // TODO: Replace toTime_t() with toSecsSinceEpoch() when minimum Qt >= 5.8
- current = qToBigEndian(static_cast<quint64>(QDateTime::currentDateTime().toTime_t()) / step);
+ current = qToBigEndian(static_cast<quint64>(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(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(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())), 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(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(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 <QSignalSpy>
@@ -26,6 +27,11 @@
QTEST_GUILESS_MAIN(TestGroup)
+namespace
+{
+ TestClock* m_clock = nullptr;
+}
+
void TestGroup::initTestCase()
{
qRegisterMetaType<Entry*>("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<Group> 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<QString> 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*>("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<Database> dbSource(createTestDatabase());
+ QScopedPointer<Database> 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<Database> dbDestination(createTestDatabase());
+ QScopedPointer<Database> 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<Database> dbDestination(createTestDatabase());
+ QScopedPointer<Database> 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<Group> groupSourceInitial = dbSource->rootGroup()->findChildByName("group1");
+ QVERIFY(groupSourceInitial != nullptr);
+ QCOMPARE(groupSourceInitial->entries().size(), 2);
+
+ QPointer<Group> groupDestinationInitial = dbSource->rootGroup()->findChildByName("group1");
+ QVERIFY(groupDestinationInitial != nullptr);
+ QCOMPARE(groupDestinationInitial->entries().size(), 2);
- Entry* entry1 = dbSource->rootGroup()->findEntry("entry1");
- QVERIFY(entry1 != nullptr);
+ QPointer<Entry> 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<Group> 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<Entry> 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<Database> dbDestination(createTestDatabase());
+ QScopedPointer<Database> 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<Group> groupSourceInitial = dbSource->rootGroup()->findChildByName("group1");
+ QVERIFY(groupSourceInitial != nullptr);
+ QCOMPARE(groupSourceInitial->entries().size(), 2);
- Entry* entry1 = dbSource->rootGroup()->findEntry("entry1");
- QVERIFY(entry1 != nullptr);
+ QPointer<Group> groupDestinationInitial = dbDestination->rootGroup()->findChildByName("group1");
+ QVERIFY(groupDestinationInitial != nullptr);
+ QCOMPARE(groupSourceInitial->entries().size(), 2);
- // 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();
+ QPointer<Entry> entrySourceInitial = dbSource->rootGroup()->findEntry("entry1");
+ QVERIFY(entrySourceInitial != nullptr);
+ QVERIFY(entrySourceInitial->group() == groupSourceInitial);
- entry1 = dbDestination->rootGroup()->findEntry("entry1");
- QVERIFY(entry1 != nullptr);
+ 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("password2");
- entry1->endUpdate();
+ m_clock->advanceSecond(1);
+ // make this entry older than in destination db
+ entrySourceInitial->beginUpdate();
+ entrySourceInitial->setPassword("password1");
+ entrySourceInitial->endUpdate();
+
+ const TimeInfo entrySourceUpdatedOlderTimeInfo = entrySourceInitial->timeInfo();
+ const TimeInfo groupSourceUpdatedOlderTimeInfo = groupSourceInitial->timeInfo();
+
+ QPointer<Group> groupDestinationUpdated = dbDestination->rootGroup()->findChildByName("group1");
+ QVERIFY(groupDestinationUpdated != nullptr);
+ QCOMPARE(groupDestinationUpdated->entries().size(), 2);
+ QPointer<Entry> entryDestinationUpdated = dbDestination->rootGroup()->findEntry("entry1");
+ QVERIFY(entryDestinationUpdated != nullptr);
+ QVERIFY(entryDestinationUpdated->group() == groupDestinationUpdated);
- dbDestination->merge(dbSource);
+ // Make sure the two changes have a different timestamp.
+ m_clock->advanceSecond(1);
+ // make this entry newer than in source db
+ entryDestinationUpdated->beginUpdate();
+ entryDestinationUpdated->setPassword("password2");
+ entryDestinationUpdated->endUpdate();
+
+ 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<Group> 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<Entry> 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<Database> dbDestination(createTestDatabase());
+ QScopedPointer<Database> 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<Entry> updatedDestinationEntry = dbDestination->rootGroup()->children().at(0)->entries().at(0);
+ const TimeInfo initialEntryTimeInfo = updatedDestinationEntry->timeInfo();
+ const TimeInfo updatedEntryTimeInfo = modificationTime(initialEntryTimeInfo, 1, 0, 0);
+
+ updatedDestinationEntry->setTimeInfo(updatedEntryTimeInfo);
- dbDestination->rootGroup()->setMergeMode(Group::MergeMode::KeepBoth);
+ dbDestination->rootGroup()->setMergeMode(Group::MergeMode::Duplicate);
- dbDestination->merge(dbSource);
+ // 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<Entry> newerEntry = dbDestination->rootGroup()->children().at(0)->entries().at(0);
+ QPointer<Entry> 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.");
+}
+
+void TestMerge::testResolveConflictTemplate(int mergeMode, std::function<void(Database*, const QMap<const char*, QDateTime>&)> verification)
+{
+ QMap<const char*, QDateTime> timestamps;
+ timestamps["initialTime"] = m_clock->currentDateTimeUtc();
+ QScopedPointer<Database> dbDestination(createTestDatabase());
+
+ 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<Database> 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<Entry> destinationEntry1 = dbDestination->rootGroup()->children().at(0)->entries().at(0);
+ QPointer<Entry> destinationEntry2 = dbDestination->rootGroup()->children().at(0)->entries().at(1);
+ QPointer<Entry> sourceEntry1 = dbSource->rootGroup()->children().at(0)->entries().at(0);
+ QPointer<Entry> 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<Entry> deletedEntryDestination = dbDestination->rootGroup()->findEntry("deletedDestination");
+ dbDestination->recycleEntry(deletedEntryDestination);
+ QPointer<Entry> 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<Group::MergeMode>(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<Group> 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<Group> mergedGroup1 = mergedRootGroup->children().at(0);
+ QPointer<Group> 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<void(Database*, const QMap<QString, QUuid>&)> verification)
+{
+ QMap<QString, QUuid> identifiers;
+ m_clock->currentDateTimeUtc();
+ QScopedPointer<Database> 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<Database> dbSource(
+ createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneIncludeHistory, Group::CloneIncludeEntries));
+
+ QPointer<Entry> sourceEntryDeletedInSourceBeforeChangedInTarget =
+ dbSource->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInSourceBeforeChangedInTarget"]);
+ QPointer<Entry> targetEntryDeletedInSourceBeforeChangedInTarget =
+ dbDestination->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInSourceBeforeChangedInTarget"]);
+
+ QPointer<Entry> sourceEntryDeletedInSourceAfterChangedInTarget =
+ dbSource->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInSourceAfterChangedInTarget"]);
+ QPointer<Entry> targetEntryDeletedInSourceAfterChangedInTarget =
+ dbDestination->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInSourceAfterChangedInTarget"]);
+
+ QPointer<Entry> sourceEntryDeletedInTargetBeforeChangedInSource =
+ dbSource->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInTargetBeforeChangedInSource"]);
+ QPointer<Entry> targetEntryDeletedInTargetBeforeChangedInSource =
+ dbDestination->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInTargetBeforeChangedInSource"]);
+
+ QPointer<Entry> sourceEntryDeletedInTargetAfterChangedInSource =
+ dbSource->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInTargetAfterChangedInSource"]);
+ QPointer<Entry> targetEntryDeletedInTargetAfterChangedInSource =
+ dbDestination->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInTargetAfterChangedInSource"]);
+
+ QPointer<Group> sourceGroupDeletedInSourceBeforeEntryUpdatedInTarget =
+ dbSource->rootGroup()->findGroupByUuid(identifiers["GroupDeletedInSourceBeforeEntryUpdatedInTarget"]);
+ QPointer<Entry> targetEntryDeletedInSourceBeforeEntryUpdatedInTarget =
+ dbDestination->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInSourceBeforeEntryUpdatedInTarget"]);
+
+ QPointer<Group> sourceGroupDeletedInSourceAfterEntryUpdatedInTarget =
+ dbSource->rootGroup()->findGroupByUuid(identifiers["GroupDeletedInSourceAfterEntryUpdatedInTarget"]);
+ QPointer<Entry> targetEntryDeletedInSourceAfterEntryUpdatedInTarget =
+ dbDestination->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInSourceAfterEntryUpdatedInTarget"]);
+
+ QPointer<Group> targetGroupDeletedInTargetBeforeEntryUpdatedInSource =
+ dbDestination->rootGroup()->findGroupByUuid(identifiers["GroupDeletedInTargetBeforeEntryUpdatedInSource"]);
+ QPointer<Entry> sourceEntryDeletedInTargetBeforeEntryUpdatedInSource =
+ dbSource->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInTargetBeforeEntryUpdatedInSource"]);
+
+ QPointer<Group> targetGroupDeletedInTargetAfterEntryUpdatedInSource =
+ dbDestination->rootGroup()->findGroupByUuid(identifiers["GroupDeletedInTargetAfterEntryUpdatedInSource"]);
+ QPointer<Entry> 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<Group::MergeMode>(mergeMode));
+
+ Merger merger(dbSource.data(), dbDestination.data());
+ merger.merge();
+
+ verification(dbDestination.data(), identifiers);
+}
+
+void TestMerge::assertDeletionNewerOnly(Database* db, const QMap<QString, QUuid>& identifiers)
+{
+ QPointer<Group> 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<QString, QUuid> &identifiers)
+{
+ QPointer<Group> 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<const char *, QDateTime> &timestamps)
+{
+ 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<const char *, QDateTime> &timestamps)
+{
+ 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<const char *, QDateTime> &timestamps)
+{
+ 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<const char *, QDateTime> &timestamps)
+{
+ 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);
+}
- QVERIFY2(olderEntry->uuid() != updatedEntry->uuid(), "KeepBoth should not reuse the UUIDs when cloning.");
+void TestMerge::testDeletionConflictEntry_Duplicate()
+{
+ testDeletionConflictTemplate(Group::Duplicate, &TestMerge::assertDeletionLocalOnly);
+}
- delete dbSource;
- delete dbDestination;
+/**
+ * Tests the KeepNewer mode concerning history.
+ */
+void TestMerge::testResolveConflictEntry_Synchronize()
+{
+ testResolveConflictTemplate(Group::Synchronize, [](Database* db, const QMap<const char*, QDateTime>& timestamps) {
+ QPointer<Group> mergedRootGroup = db->rootGroup();
+ QPointer<Group> 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<const char*, QDateTime>& timestamps) {
+ QPointer<Group> mergedRootGroup = db->rootGroup();
+ QPointer<Group> 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<const char*, QDateTime>& timestamps) {
+ QPointer<Group> mergedRootGroup = db->rootGroup();
+ QPointer<Group> 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<const char*, QDateTime>& timestamps) {
+ QPointer<Group> mergedRootGroup = db->rootGroup();
+ QPointer<Group> 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();
-
- Database* dbSource = new Database();
- dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries));
+ QScopedPointer<Database> dbDestination(createTestDatabase());
+ QScopedPointer<Database> dbSource(
+ createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
- Entry* entry1 = dbSource->rootGroup()->findEntry("entry1");
- QVERIFY(entry1 != nullptr);
+ QPointer<Entry> entrySourceInitial = dbSource->rootGroup()->findEntry("entry1");
+ QVERIFY(entrySourceInitial != nullptr);
- Group* group2 = dbSource->rootGroup()->findChildByName("group2");
- QVERIFY(group2 != nullptr);
+ QPointer<Group> 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"));
- QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
+ m_clock->advanceSecond(1);
+
+ Merger merger(dbSource.data(), dbDestination.data());
+ merger.merge();
- delete dbDestination;
- delete dbSource;
+ QPointer<Entry> entryDestinationMerged = dbDestination->rootGroup()->findEntry("entry1");
+ QVERIFY(entryDestinationMerged != nullptr);
+ QCOMPARE(entryDestinationMerged->group()->name(), QString("group2"));
+ QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
}
/**
@@ -262,95 +820,96 @@ void TestMerge::testMoveEntry()
*/
void TestMerge::testMoveEntryPreserveChanges()
{
- Database* dbDestination = createTestDatabase();
+ QScopedPointer<Database> dbDestination(createTestDatabase());
+ QScopedPointer<Database> dbSource(
+ createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
- Database* dbSource = new Database();
- dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries));
+ QPointer<Entry> entrySourceInitial = dbSource->rootGroup()->findEntry("entry1");
+ QVERIFY(entrySourceInitial != nullptr);
- Entry* entry1 = dbSource->rootGroup()->findEntry("entry1");
- QVERIFY(entry1 != nullptr);
+ QPointer<Group> 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<Entry> 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"));
- QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
- QCOMPARE(entry1->password(), QString("password"));
+ m_clock->advanceSecond(1);
- delete dbDestination;
- delete dbSource;
+ Merger merger(dbSource.data(), dbDestination.data());
+ merger.merge();
+
+ QPointer<Entry> entryDestinationMerged = dbDestination->rootGroup()->findEntry("entry1");
+ QVERIFY(entryDestinationMerged != nullptr);
+ QCOMPARE(entryDestinationMerged->group()->name(), QString("group2"));
+ QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
+ QCOMPARE(entryDestinationMerged->password(), QString("password"));
}
void TestMerge::testCreateNewGroups()
{
- Database* dbDestination = createTestDatabase();
+ QScopedPointer<Database> dbDestination(createTestDatabase());
+ QScopedPointer<Database> 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<Group> groupDestinationMerged = dbDestination->rootGroup()->findChildByName("group3");
+ QVERIFY(groupDestinationMerged != nullptr);
+ QCOMPARE(groupDestinationMerged->name(), QString("group3"));
}
void TestMerge::testMoveEntryIntoNewGroup()
{
- Database* dbDestination = createTestDatabase();
+ QScopedPointer<Database> dbDestination(createTestDatabase());
+ QScopedPointer<Database> 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<Entry> entrySourceMoved = dbSource->rootGroup()->findEntry("entry1");
+ entrySourceMoved->setGroup(groupSourceCreated);
- dbDestination->merge(dbSource);
+ m_clock->advanceSecond(1);
- QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
+ Merger merger(dbSource.data(), dbDestination.data());
+ merger.merge();
- group3 = dbDestination->rootGroup()->findChildByName("group3");
- QVERIFY(group3 != nullptr);
- QCOMPARE(group3->name(), QString("group3"));
- QCOMPARE(group3->entries().size(), 1);
+ QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
- entry1 = dbDestination->rootGroup()->findEntry("entry1");
- QVERIFY(entry1 != nullptr);
- QCOMPARE(entry1->group()->name(), QString("group3"));
+ QPointer<Group> groupDestinationMerged = dbDestination->rootGroup()->findChildByName("group3");
+ QVERIFY(groupDestinationMerged != nullptr);
+ QCOMPARE(groupDestinationMerged->name(), QString("group3"));
+ QCOMPARE(groupDestinationMerged->entries().size(), 1);
- delete dbDestination;
- delete dbSource;
+ QPointer<Entry> 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<Database> dbDestination(createTestDatabase());
+ QScopedPointer<Database> 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<Entry> 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<Entry> entrySourceMoved = dbSource->rootGroup()->findEntry("entry1");
+ QVERIFY(entrySourceMoved != nullptr);
+ entrySourceMoved->beginUpdate();
+ entrySourceMoved->setUsername("username");
+ entrySourceMoved->endUpdate();
+ QDateTime sourceLocationChanged = entrySourceMoved->timeInfo().locationChanged();
- QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
+ QVERIFY(destinationLocationChanged > sourceLocationChanged);
- 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());
+ m_clock->advanceSecond(1);
+
+ Merger merger(dbSource.data(), dbDestination.data());
+ merger.merge();
+
+ QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
- delete dbDestination;
- delete dbSource;
+ QPointer<Entry> 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<Database> dbDestination(createTestDatabase());
+ QScopedPointer<Database> 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<Group> 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<Entry> 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);
- QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
+ Merger merger(dbSource.data(), dbDestination.data());
+ merger.merge();
- entry1 = dbDestination->rootGroup()->findEntry("entry1 renamed");
- QVERIFY(entry1 != nullptr);
- QVERIFY(entry1->group() != nullptr);
- QCOMPARE(entry1->group()->name(), QString("group2 renamed"));
- QCOMPARE(uuidBeforeSyncing, entry1->uuid());
+ QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
- group2 = dbDestination->rootGroup()->findChildByName("group2 renamed");
- QCOMPARE(group2->notes(), QString("updated notes"));
- QCOMPARE(group2->iconUuid(), customIconId);
+ QPointer<Entry> entryDestinationMerged = dbDestination->rootGroup()->findEntry("entry1 renamed");
+ QVERIFY(entryDestinationMerged != nullptr);
+ QVERIFY(entryDestinationMerged->group() != nullptr);
+ QCOMPARE(entryDestinationMerged->group()->name(), QString("group2 renamed"));
+ QCOMPARE(uuidBeforeSyncing, entryDestinationMerged->uuid());
- delete dbDestination;
- delete dbSource;
+ QPointer<Group> 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<Database> 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<Database> dbSource(
+ createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
// Sanity check
- group3 = dbSource->rootGroup()->findChildByUuid(group3Uuid);
- QVERIFY(group3 != nullptr);
+ QPointer<Group> group3SourceInitial = dbSource->rootGroup()->findGroupByUuid(group3Uuid);
+ QVERIFY(group3DestinationCreated != nullptr);
+
+ QDateTime initialLocationChanged = group3SourceInitial->timeInfo().locationChanged();
+
+ m_clock->advanceSecond(1);
+
+ QPointer<Group> group3SourceMoved = dbSource->rootGroup()->findGroupByUuid(group3Uuid);
+ QVERIFY(group3SourceMoved != nullptr);
+ group3SourceMoved->setParent(dbSource->rootGroup()->findChildByName("group2"));
- QTest::qSleep(1);
+ QDateTime movedLocaltionChanged = group3SourceMoved->timeInfo().locationChanged();
+ QVERIFY(initialLocationChanged < movedLocaltionChanged);
- 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"));
+ Merger merger1(dbSource.data(), dbDestination.data());
+ merger1.merge();
- dbDestination->merge(dbSource);
- group3 = dbDestination->rootGroup()->findChildByUuid(group3Uuid);
- QVERIFY(group3 != nullptr);
- QCOMPARE(group3->parent(), dbDestination->rootGroup()->findChildByName("group2"));
+ QPointer<Group> group3DestinationMerged1 = dbDestination->rootGroup()->findGroupByUuid(group3Uuid);
+ QVERIFY(group3DestinationMerged1 != nullptr);
+ QCOMPARE(group3DestinationMerged1->parent(), dbDestination->rootGroup()->findChildByName("group2"));
+ QCOMPARE(group3DestinationMerged1->timeInfo().locationChanged(), movedLocaltionChanged);
- delete dbDestination;
- delete dbSource;
+ m_clock->advanceSecond(1);
+
+ Merger merger2(dbSource.data(), dbDestination.data());
+ merger2.merge();
+
+ QPointer<Group> 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<Database> dbDestination(new Database());
+ QScopedPointer<Database> 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<Database> dbDestination(new Database());
+ QScopedPointer<Database> 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));
+}
+
+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<Database> dbDestination(createTestDatabase());
+ QScopedPointer<Database> dbSource(
+ createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
- delete dbDestination;
- delete dbSource;
+ m_clock->advanceSecond(1);
+
+ QPointer<Entry> entry1SourceInitial = dbSource->rootGroup()->findEntry("entry1");
+ QVERIFY(entry1SourceInitial != nullptr);
+ QUuid entry1Uuid = entry1SourceInitial->uuid();
+ delete entry1SourceInitial;
+ QVERIFY(dbSource->containsDeletedObject(entry1Uuid));
+
+ m_clock->advanceSecond(1);
+
+ QPointer<Entry> 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<Entry> entry1DestinationMerged = dbDestination->rootGroup()->findEntry("entry1");
+ QVERIFY(entry1DestinationMerged);
+ QVERIFY(!dbDestination->containsDeletedObject(entry1Uuid));
+ QPointer<Entry> 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<Database> dbDestination(createTestDatabase());
+ QScopedPointer<Database> dbSource(
+ createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
+
+ m_clock->advanceSecond(1);
+
+ QPointer<Group> 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<Group> group1SourceInitial = dbSource->rootGroup()->findChildByName("group1");
+ QVERIFY(group1SourceInitial != nullptr);
+ QPointer<Entry> entry1SourceInitial = dbSource->rootGroup()->findEntry("entry1");
+ QVERIFY(entry1SourceInitial != nullptr);
+ QPointer<Entry> 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<Group> 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<Entry> entry1DestinationMerged = dbDestination->rootGroup()->findEntry("entry1");
+ QVERIFY(entry1DestinationMerged);
+ QPointer<Entry> entry2DestinationMerged = dbDestination->rootGroup()->findEntry("entry2");
+ QVERIFY(entry2DestinationMerged);
+ QPointer<Entry> entry3DestinationMerged = dbDestination->rootGroup()->findEntry("entry3");
+ QVERIFY(entry3DestinationMerged);
+ QPointer<Group> group1DestinationMerged = dbDestination->rootGroup()->findChildByName("group1");
+ QVERIFY(group1DestinationMerged);
+ QPointer<Group> group2DestinationMerged = dbDestination->rootGroup()->findChildByName("group2");
+ QVERIFY(group2DestinationMerged);
+
+ QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 3);
+}
+
+void TestMerge::testDeletedRevertedEntry()
+{
+ QScopedPointer<Database> dbDestination(createTestDatabase());
+ QScopedPointer<Database> dbSource(
+ createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
+
+ m_clock->advanceSecond(1);
+
+ QPointer<Entry> entry1DestinationInitial = dbDestination->rootGroup()->findEntry("entry1");
+ QVERIFY(entry1DestinationInitial != nullptr);
+ QUuid entry1Uuid = entry1DestinationInitial->uuid();
+ delete entry1DestinationInitial;
+ QVERIFY(dbDestination->containsDeletedObject(entry1Uuid));
+
+ m_clock->advanceSecond(1);
+
+ QPointer<Entry> entry2SourceInitial = dbSource->rootGroup()->findEntry("entry2");
+ QVERIFY(entry2SourceInitial != nullptr);
+ QUuid entry2Uuid = entry2SourceInitial->uuid();
+ delete entry2SourceInitial;
+ QVERIFY(dbSource->containsDeletedObject(entry2Uuid));
+
+ m_clock->advanceSecond(1);
+
+ QPointer<Entry> entry1SourceInitial = dbSource->rootGroup()->findEntry("entry1");
+ QVERIFY(entry1SourceInitial != nullptr);
+ entry1SourceInitial->setNotes("Updated");
+
+ QPointer<Entry> 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<Entry> entry1DestinationMerged = dbDestination->rootGroup()->findEntry("entry1");
+ QVERIFY(entry1DestinationMerged);
+ QVERIFY(entry1DestinationMerged->notes() == "Updated");
+ QPointer<Entry> entry2DestinationMerged = dbDestination->rootGroup()->findEntry("entry2");
+ QVERIFY(entry2DestinationMerged);
+ QVERIFY(entry2DestinationMerged->notes() == "Updated");
+}
+
+void TestMerge::testDeletedRevertedGroup()
+{
+ QScopedPointer<Database> dbDestination(createTestDatabase());
+ QScopedPointer<Database> dbSource(
+ createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
+
+ m_clock->advanceSecond(1);
+
+ QPointer<Group> group2SourceInitial = dbSource->rootGroup()->findChildByName("group2");
+ QVERIFY(group2SourceInitial);
+ QUuid group2Uuid = group2SourceInitial->uuid();
+ delete group2SourceInitial;
+ QVERIFY(dbSource->containsDeletedObject(group2Uuid));
+
+ m_clock->advanceSecond(1);
+
+ QPointer<Group> group1DestinationInitial = dbDestination->rootGroup()->findChildByName("group1");
+ QVERIFY(group1DestinationInitial);
+ QUuid group1Uuid = group1DestinationInitial->uuid();
+ delete group1DestinationInitial;
+ QVERIFY(dbDestination->containsDeletedObject(group1Uuid));
+
+ m_clock->advanceSecond(1);
+
+ QPointer<Group> group1SourceInitial = dbSource->rootGroup()->findChildByName("group1");
+ QVERIFY(group1SourceInitial);
+ group1SourceInitial->setNotes("Updated");
+
+ m_clock->advanceSecond(1);
+
+ QPointer<Group> 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<Group> group1DestinationMerged = dbDestination->rootGroup()->findChildByName("group1");
+ QVERIFY(group1DestinationMerged);
+ QVERIFY(group1DestinationMerged->notes() == "Updated");
+ QPointer<Group> 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<Database> dbDestination(createTestDatabase());
+ QScopedPointer<Database> dbSource(
+ createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
// sanity check
- Group* group1 = dbSource->rootGroup()->findChildByName("group1");
- QVERIFY(group1 != nullptr);
+ QPointer<Group> 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<Group> groupDestinationUpdated = dbDestination->rootGroup()->findChildByName("group1");
+ groupDestinationUpdated->setName("group1 updated in destination");
- dbDestination->merge(dbSource);
+ m_clock->advanceSecond(1);
- // sanity check
- group1 = dbDestination->rootGroup()->findChildByName("group1 updated in destination");
- QVERIFY(group1 != nullptr);
+ Merger merger(dbSource.data(), dbDestination.data());
+ merger.merge();
- delete dbDestination;
- delete dbSource;
+ // sanity check
+ QPointer<Group> 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<Entry::CloneFlag>(entryFlags),
+ static_cast<Group::CloneFlag>(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 <QDateTime>
+#include <QMap>
#include <QObject>
+#include <functional>
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<void(Database*, const QMap<const char*, QDateTime>&)> verification);
+ void testDeletionConflictTemplate(int mergeMode, std::function<void(Database*, const QMap<QString, QUuid>&)> verification);
+ static void assertDeletionNewerOnly(Database *db, const QMap<QString, QUuid> &identifiers);
+ static void assertDeletionLocalOnly(Database *db, const QMap<QString, QUuid> &identifiers);
+ static void assertUpdateMergedEntry1(Entry *entry, const QMap<const char*, QDateTime> &timestamps);
+ static void assertUpdateReappliedEntry2(Entry *entry, const QMap<const char*, QDateTime> &timestamps);
+ static void assertUpdateReappliedEntry1(Entry *entry, const QMap<const char*, QDateTime> &timestamps);
+ static void assertUpdateMergedEntry2(Entry *entry, const QMap<const char *, QDateTime> &timestamps);
+
};
#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 <QSignalSpy>
#include <QTest>
+#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<qint64>(tmpData.size())));
+ QVERIFY(m_dbFile.write(unmodifiedMergeDatabase, static_cast<qint64>(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<qint64>(tmpData.size())));
+ QVERIFY(m_dbFile.write(unmodifiedMergeDatabase, static_cast<qint64>(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<qint64>(tmpData.size())));
+ QVERIFY(m_dbFile.write(unmodifiedMergeDatabase, static_cast<qint64>(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 <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "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 <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef KEEPASSXC_TESTCLOCK_H
+#define KEEPASSXC_TESTCLOCK_H
+
+#include "core/Clock.h"
+
+#include <QDateTime>
+
+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