keepassxc/tests/TestMerge.cpp
Jonathan White 3829bcdd8f Prevent KeeShare from merging database custom data
This issue previously caused parent databases to be marked as modified on unlock. This was because of the new protections against byte-by-byte side channel attacks adds a randomized string to the database custom data. We should never be merging database custom data with keeshare or imports since we are merging groups only.

Also prevent overwrite of auto-generated custom data fields, Last Modified and Random Slug.
2024-04-29 08:50:46 -04:00

1500 lines
66 KiB
C++

/*
* Copyright (C) 2017 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 "TestMerge.h"
#include "mock/MockClock.h"
#include "core/Merger.h"
#include "core/Metadata.h"
#include "crypto/Crypto.h"
#include <QSignalSpy>
#include <QTest>
QTEST_GUILESS_MAIN(TestMerge)
namespace
{
MockClock* m_clock = nullptr;
} // namespace
void TestMerge::initTestCase()
{
qRegisterMetaType<Entry*>("Entry*");
qRegisterMetaType<Group*>("Group*");
QVERIFY(Crypto::init());
}
void TestMerge::init()
{
Q_ASSERT(m_clock == nullptr);
m_clock = new MockClock(2010, 5, 5, 10, 30, 10);
MockClock::setup(m_clock);
}
void TestMerge::cleanup()
{
MockClock::teardown();
m_clock = nullptr;
}
/**
* Merge an existing database into a new one.
* All the entries of the existing should end
* up in the new one.
*/
void TestMerge::testMergeIntoNew()
{
QScopedPointer<Database> dbSource(createTestDatabase());
QScopedPointer<Database> dbDestination(new Database());
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);
}
/**
* Merging when no changes occurred should not
* have any side effect.
*/
void TestMerge::testMergeNoChanges()
{
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);
m_clock->advanceSecond(1);
Merger merger1(dbSource.data(), dbDestination.data());
auto changes = merger1.merge();
QVERIFY(changes.isEmpty());
QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
QCOMPARE(dbSource->rootGroup()->entriesRecursive().size(), 2);
m_clock->advanceSecond(1);
Merger merger2(dbSource.data(), dbDestination.data());
changes = merger2.merge();
QVERIFY(changes.isEmpty());
QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
QCOMPARE(dbSource->rootGroup()->entriesRecursive().size(), 2);
}
/**
* Merging without database custom data (used by imports and KeeShare)
*/
void TestMerge::testMergeCustomData()
{
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->metadata()->customData()->set("TEST_CUSTOM_DATA", "OLD TESTING");
m_clock->advanceSecond(1);
dbSource->metadata()->customData()->set("TEST_CUSTOM_DATA", "TESTING");
// First check that the custom data is not merged when skipped
Merger merger1(dbSource.data(), dbDestination.data());
merger1.setSkipDatabaseCustomData(true);
auto changes = merger1.merge();
QVERIFY(changes.isEmpty());
QCOMPARE(dbDestination->metadata()->customData()->value("TEST_CUSTOM_DATA"), QString("OLD TESTING"));
// Second check that the custom data is merged otherwise
Merger merger2(dbSource.data(), dbDestination.data());
changes = merger2.merge();
QCOMPARE(changes.size(), 1);
QCOMPARE(dbDestination->metadata()->customData()->value("TEST_CUSTOM_DATA"), QString("TESTING"));
}
/**
* If the entry is updated in the source database, the update
* should propagate in the destination database.
*/
void TestMerge::testResolveConflictNewer()
{
QScopedPointer<Database> dbDestination(createTestDatabase());
QScopedPointer<Database> dbSource(
createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
// sanity check
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);
QPointer<Entry> entrySourceInitial = dbSource->rootGroup()->findEntryByPath("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.
m_clock->advanceSecond(1);
// make this entry newer than in destination db
entrySourceInitial->beginUpdate();
entrySourceInitial->setPassword("password");
entrySourceInitial->endUpdate();
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
QPointer<Group> groupDestinationMerged = dbDestination->rootGroup()->findChildByName("group1");
QVERIFY(groupDestinationMerged != nullptr);
QCOMPARE(groupDestinationMerged->entries().size(), 2);
QCOMPARE(groupDestinationMerged->timeInfo(), groupDestinationInitialTimeInfo);
QPointer<Entry> entryDestinationMerged = dbDestination->rootGroup()->findEntryByPath("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 != entryDestinationMerged->uuid());
}
}
/**
* If the entry is updated in the source database, and the
* destination database after, the entry should remain the
* same.
*/
void TestMerge::testResolveConflictExisting()
{
QScopedPointer<Database> dbDestination(createTestDatabase());
QScopedPointer<Database> dbSource(
createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
// sanity check
QPointer<Group> groupSourceInitial = dbSource->rootGroup()->findChildByName("group1");
QVERIFY(groupSourceInitial != nullptr);
QCOMPARE(groupSourceInitial->entries().size(), 2);
QPointer<Group> groupDestinationInitial = dbDestination->rootGroup()->findChildByName("group1");
QVERIFY(groupDestinationInitial != nullptr);
QCOMPARE(groupSourceInitial->entries().size(), 2);
QPointer<Entry> entrySourceInitial = dbSource->rootGroup()->findEntryByPath("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.
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()->findEntryByPath("entry1");
QVERIFY(entryDestinationUpdated != nullptr);
QVERIFY(entryDestinationUpdated->group() == groupDestinationUpdated);
// 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
QPointer<Group> groupDestinationMerged = dbDestination->rootGroup()->findChildByName("group1");
QVERIFY(groupDestinationMerged != nullptr);
QCOMPARE(groupDestinationMerged->entries().size(), 2);
QCOMPARE(groupDestinationMerged->timeInfo(), groupDestinationUpdatedNewerTimeInfo);
QPointer<Entry> entryDestinationMerged = dbDestination->rootGroup()->findEntryByPath("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 != entryDestinationMerged->uuid());
}
}
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());
auto deletedEntry1 = new Entry();
deletedEntry1->setUuid(QUuid::createUuid());
deletedEntry1->beginUpdate();
deletedEntry1->setGroup(dbDestination->rootGroup());
deletedEntry1->setTitle("deletedDestination");
deletedEntry1->endUpdate();
auto 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()->findEntryByPath("deletedDestination");
dbDestination->recycleEntry(deletedEntryDestination);
QPointer<Entry> deletedEntrySource = dbSource->rootGroup()->findEntryByPath("deletedSource");
dbSource->recycleEntry(deletedEntrySource);
m_clock->advanceMinute(1);
auto destinationEntrySingle = new Entry();
destinationEntrySingle->setUuid(QUuid::createUuid());
destinationEntrySingle->beginUpdate();
destinationEntrySingle->setGroup(dbDestination->rootGroup()->children().at(1));
destinationEntrySingle->setTitle("entryDestination");
destinationEntrySingle->endUpdate();
auto 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()->findEntryByPath("entryDestination"));
QVERIFY(dbDestination->rootGroup()->findEntryByPath("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) {
auto 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) {
auto 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 prevents 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 prevents 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_KeepNewer()
{
testDeletionConflictTemplate(Group::KeepNewer, &TestMerge::assertDeletionLocalOnly);
}
/**
* 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);
});
}
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);
});
}
/**
* The location of an entry should be updated in the
* destination database.
*/
void TestMerge::testMoveEntry()
{
QScopedPointer<Database> dbDestination(createTestDatabase());
QScopedPointer<Database> dbSource(
createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
QPointer<Entry> entrySourceInitial = dbSource->rootGroup()->findEntryByPath("entry1");
QVERIFY(entrySourceInitial != nullptr);
QPointer<Group> groupSourceInitial = dbSource->rootGroup()->findChildByName("group2");
QVERIFY(groupSourceInitial != nullptr);
// Make sure the two changes have a different timestamp.
m_clock->advanceSecond(1);
entrySourceInitial->setGroup(groupSourceInitial);
QCOMPARE(entrySourceInitial->group()->name(), QString("group2"));
m_clock->advanceSecond(1);
Merger merger(dbSource.data(), dbDestination.data());
merger.merge();
QPointer<Entry> entryDestinationMerged = dbDestination->rootGroup()->findEntryByPath("entry1");
QVERIFY(entryDestinationMerged != nullptr);
QCOMPARE(entryDestinationMerged->group()->name(), QString("group2"));
QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
}
/**
* The location of an entry should be updated in the
* destination database, but changes from the destination
* database should be preserved.
*/
void TestMerge::testMoveEntryPreserveChanges()
{
QScopedPointer<Database> dbDestination(createTestDatabase());
QScopedPointer<Database> dbSource(
createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
QPointer<Entry> entrySourceInitial = dbSource->rootGroup()->findEntryByPath("entry1");
QVERIFY(entrySourceInitial != nullptr);
QPointer<Group> group2Source = dbSource->rootGroup()->findChildByName("group2");
QVERIFY(group2Source != nullptr);
m_clock->advanceSecond(1);
entrySourceInitial->setGroup(group2Source);
QCOMPARE(entrySourceInitial->group()->name(), QString("group2"));
QPointer<Entry> entryDestinationInitial = dbDestination->rootGroup()->findEntryByPath("entry1");
QVERIFY(entryDestinationInitial != nullptr);
m_clock->advanceSecond(1);
entryDestinationInitial->beginUpdate();
entryDestinationInitial->setPassword("password");
entryDestinationInitial->endUpdate();
m_clock->advanceSecond(1);
Merger merger(dbSource.data(), dbDestination.data());
merger.merge();
QPointer<Entry> entryDestinationMerged = dbDestination->rootGroup()->findEntryByPath("entry1");
QVERIFY(entryDestinationMerged != nullptr);
QCOMPARE(entryDestinationMerged->group()->name(), QString("group2"));
QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
QCOMPARE(entryDestinationMerged->password(), QString("password"));
}
void TestMerge::testCreateNewGroups()
{
QScopedPointer<Database> dbDestination(createTestDatabase());
QScopedPointer<Database> dbSource(
createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
m_clock->advanceSecond(1);
auto groupSourceCreated = new Group();
groupSourceCreated->setName("group3");
groupSourceCreated->setUuid(QUuid::createUuid());
groupSourceCreated->setParent(dbSource->rootGroup());
m_clock->advanceSecond(1);
Merger merger(dbSource.data(), dbDestination.data());
merger.merge();
QPointer<Group> groupDestinationMerged = dbDestination->rootGroup()->findChildByName("group3");
QVERIFY(groupDestinationMerged != nullptr);
QCOMPARE(groupDestinationMerged->name(), QString("group3"));
}
void TestMerge::testMoveEntryIntoNewGroup()
{
QScopedPointer<Database> dbDestination(createTestDatabase());
QScopedPointer<Database> dbSource(
createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
m_clock->advanceSecond(1);
auto groupSourceCreated = new Group();
groupSourceCreated->setName("group3");
groupSourceCreated->setUuid(QUuid::createUuid());
groupSourceCreated->setParent(dbSource->rootGroup());
QPointer<Entry> entrySourceMoved = dbSource->rootGroup()->findEntryByPath("entry1");
entrySourceMoved->setGroup(groupSourceCreated);
m_clock->advanceSecond(1);
Merger merger(dbSource.data(), dbDestination.data());
merger.merge();
QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
QPointer<Group> groupDestinationMerged = dbDestination->rootGroup()->findChildByName("group3");
QVERIFY(groupDestinationMerged != nullptr);
QCOMPARE(groupDestinationMerged->name(), QString("group3"));
QCOMPARE(groupDestinationMerged->entries().size(), 1);
QPointer<Entry> entryDestinationMerged = dbDestination->rootGroup()->findEntryByPath("entry1");
QVERIFY(entryDestinationMerged != nullptr);
QCOMPARE(entryDestinationMerged->group()->name(), QString("group3"));
}
/**
* Even though the entries' locations are no longer
* the same, we will keep associating them.
*/
void TestMerge::testUpdateEntryDifferentLocation()
{
QScopedPointer<Database> dbDestination(createTestDatabase());
QScopedPointer<Database> dbSource(
createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
auto groupDestinationCreated = new Group();
groupDestinationCreated->setName("group3");
groupDestinationCreated->setUuid(QUuid::createUuid());
groupDestinationCreated->setParent(dbDestination->rootGroup());
m_clock->advanceSecond(1);
QPointer<Entry> entryDestinationMoved = dbDestination->rootGroup()->findEntryByPath("entry1");
QVERIFY(entryDestinationMoved != nullptr);
entryDestinationMoved->setGroup(groupDestinationCreated);
QUuid uuidBeforeSyncing = entryDestinationMoved->uuid();
QDateTime destinationLocationChanged = entryDestinationMoved->timeInfo().locationChanged();
// Change the entry in the source db.
m_clock->advanceSecond(1);
QPointer<Entry> entrySourceMoved = dbSource->rootGroup()->findEntryByPath("entry1");
QVERIFY(entrySourceMoved != nullptr);
entrySourceMoved->beginUpdate();
entrySourceMoved->setUsername("username");
entrySourceMoved->endUpdate();
QDateTime sourceLocationChanged = entrySourceMoved->timeInfo().locationChanged();
QVERIFY(destinationLocationChanged > sourceLocationChanged);
m_clock->advanceSecond(1);
Merger merger(dbSource.data(), dbDestination.data());
merger.merge();
QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
QPointer<Entry> entryDestinationMerged = dbDestination->rootGroup()->findEntryByPath("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 strategy is KeepNewer - therefore the older location is used!
QCOMPARE(entryDestinationMerged->timeInfo().locationChanged(), sourceLocationChanged);
}
/**
* Groups should be updated using the uuids.
*/
void TestMerge::testUpdateGroup()
{
QScopedPointer<Database> dbDestination(createTestDatabase());
QScopedPointer<Database> dbSource(
createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
m_clock->advanceSecond(1);
QPointer<Group> groupSourceInitial = dbSource->rootGroup()->findChildByName("group2");
groupSourceInitial->setName("group2 renamed");
groupSourceInitial->setNotes("updated notes");
QUuid customIconId = QUuid::createUuid();
dbSource->metadata()->addCustomIcon(customIconId, QString("custom icon").toLocal8Bit());
groupSourceInitial->setIcon(customIconId);
QPointer<Entry> entrySourceInitial = dbSource->rootGroup()->findEntryByPath("entry1");
QVERIFY(entrySourceInitial != nullptr);
entrySourceInitial->setGroup(groupSourceInitial);
entrySourceInitial->setTitle("entry1 renamed");
QUuid uuidBeforeSyncing = entrySourceInitial->uuid();
m_clock->advanceSecond(1);
Merger merger(dbSource.data(), dbDestination.data());
merger.merge();
QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
QPointer<Entry> entryDestinationMerged = dbDestination->rootGroup()->findEntryByPath("entry1 renamed");
QVERIFY(entryDestinationMerged != nullptr);
QVERIFY(entryDestinationMerged->group() != nullptr);
QCOMPARE(entryDestinationMerged->group()->name(), QString("group2 renamed"));
QCOMPARE(uuidBeforeSyncing, entryDestinationMerged->uuid());
QPointer<Group> groupMerged = dbDestination->rootGroup()->findChildByName("group2 renamed");
QCOMPARE(groupMerged->notes(), QString("updated notes"));
QCOMPARE(groupMerged->iconUuid(), customIconId);
}
void TestMerge::testUpdateGroupLocation()
{
QScopedPointer<Database> dbDestination(createTestDatabase());
auto group3DestinationCreated = new Group();
QUuid group3Uuid = QUuid::createUuid();
group3DestinationCreated->setUuid(group3Uuid);
group3DestinationCreated->setName("group3");
group3DestinationCreated->setParent(dbDestination->rootGroup()->findChildByName("group1"));
QScopedPointer<Database> dbSource(
createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
// Sanity check
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"));
QDateTime movedLocaltionChanged = group3SourceMoved->timeInfo().locationChanged();
QVERIFY(initialLocationChanged < movedLocaltionChanged);
m_clock->advanceSecond(1);
Merger merger1(dbSource.data(), dbDestination.data());
merger1.merge();
QPointer<Group> group3DestinationMerged1 = dbDestination->rootGroup()->findGroupByUuid(group3Uuid);
QVERIFY(group3DestinationMerged1 != nullptr);
QCOMPARE(group3DestinationMerged1->parent(), dbDestination->rootGroup()->findChildByName("group2"));
QCOMPARE(group3DestinationMerged1->timeInfo().locationChanged(), movedLocaltionChanged);
m_clock->advanceSecond(1);
Merger merger2(dbSource.data(), dbDestination.data());
merger2.merge();
QPointer<Group> group3DestinationMerged2 = dbDestination->rootGroup()->findGroupByUuid(group3Uuid);
QVERIFY(group3DestinationMerged2 != nullptr);
QCOMPARE(group3DestinationMerged2->parent(), dbDestination->rootGroup()->findChildByName("group2"));
QCOMPARE(group3DestinationMerged1->timeInfo().locationChanged(), movedLocaltionChanged);
}
/**
* The first merge should create new entries, the
* second should only sync them, since they have
* been created with the same UUIDs.
*/
void TestMerge::testMergeAndSync()
{
QScopedPointer<Database> dbDestination(new Database());
QScopedPointer<Database> dbSource(createTestDatabase());
QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 0);
m_clock->advanceSecond(1);
Merger merger1(dbSource.data(), dbDestination.data());
merger1.merge();
QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
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);
}
/**
* Custom icons should be brought over when merging.
*/
void TestMerge::testMergeCustomIcons()
{
QScopedPointer<Database> dbDestination(new Database());
QScopedPointer<Database> dbSource(createTestDatabase());
m_clock->advanceSecond(1);
QUuid customIconId = QUuid::createUuid();
dbSource->metadata()->addCustomIcon(customIconId, QString("custom icon").toLocal8Bit());
// Sanity check.
QVERIFY(dbSource->metadata()->hasCustomIcon(customIconId));
m_clock->advanceSecond(1);
Merger merger(dbSource.data(), dbDestination.data());
merger.merge();
QVERIFY(dbDestination->metadata()->hasCustomIcon(customIconId));
}
/**
* No duplicate icons should be created
*/
void TestMerge::testMergeDuplicateCustomIcons()
{
QScopedPointer<Database> dbDestination(new Database());
QScopedPointer<Database> dbSource(createTestDatabase());
m_clock->advanceSecond(1);
QUuid customIconId = QUuid::createUuid();
QByteArray customIcon1("custom icon 1");
QByteArray customIcon2("custom icon 2");
dbSource->metadata()->addCustomIcon(customIconId, customIcon1);
dbDestination->metadata()->addCustomIcon(customIconId, customIcon2);
// Sanity check.
QVERIFY(dbSource->metadata()->hasCustomIcon(customIconId));
QVERIFY(dbDestination->metadata()->hasCustomIcon(customIconId));
m_clock->advanceSecond(1);
Merger merger(dbSource.data(), dbDestination.data());
merger.merge();
QVERIFY(dbDestination->metadata()->hasCustomIcon(customIconId));
QCOMPARE(dbDestination->metadata()->customIconsOrder().count(), 1);
QCOMPARE(dbDestination->metadata()->customIcon(customIconId).data, customIcon2);
}
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 really 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::testCustomData()
{
QScopedPointer<Database> dbDestination(new Database());
QScopedPointer<Database> dbSource(createTestDatabase());
QScopedPointer<Database> dbDestination2(new Database());
QScopedPointer<Database> dbSource2(createTestDatabase());
m_clock->advanceSecond(1);
dbDestination->metadata()->customData()->set("toBeDeleted", "value");
dbDestination->metadata()->customData()->set("key3", "oldValue");
dbSource2->metadata()->customData()->set("key1", "value1");
dbSource2->metadata()->customData()->set("key2", "value2");
dbSource2->metadata()->customData()->set("key3", "newValue");
dbSource2->metadata()->customData()->set("Browser", "n'8=3W@L^6d->d.]St_>]");
m_clock->advanceSecond(1);
dbSource->metadata()->customData()->set("key1", "value1");
dbSource->metadata()->customData()->set("key2", "value2");
dbSource->metadata()->customData()->set("key3", "newValue");
dbSource->metadata()->customData()->set("Browser", "n'8=3W@L^6d->d.]St_>]");
dbDestination2->metadata()->customData()->set("notToBeDeleted", "value");
dbDestination2->metadata()->customData()->set("key3", "oldValue");
// Sanity check.
QVERIFY(!dbSource->metadata()->customData()->isEmpty());
QVERIFY(!dbSource2->metadata()->customData()->isEmpty());
m_clock->advanceSecond(1);
Merger merger(dbSource.data(), dbDestination.data());
QStringList changes = merger.merge();
QVERIFY(!changes.isEmpty());
// Source is newer, data should be merged
QVERIFY(!dbDestination->metadata()->customData()->isEmpty());
QVERIFY(dbDestination->metadata()->customData()->contains("key1"));
QVERIFY(dbDestination->metadata()->customData()->contains("key2"));
QVERIFY(dbDestination->metadata()->customData()->contains("Browser"));
QVERIFY(!dbDestination->metadata()->customData()->contains("toBeDeleted"));
QCOMPARE(dbDestination->metadata()->customData()->value("key1"), QString("value1"));
QCOMPARE(dbDestination->metadata()->customData()->value("key2"), QString("value2"));
QCOMPARE(dbDestination->metadata()->customData()->value("Browser"), QString("n'8=3W@L^6d->d.]St_>]"));
QCOMPARE(dbDestination->metadata()->customData()->value("key3"),
QString("newValue")); // Old value should be replaced
// Merging again should not do anything if the values are the same.
m_clock->advanceSecond(1);
dbSource->metadata()->customData()->set("key3", "oldValue");
dbSource->metadata()->customData()->set("key3", "newValue");
Merger merger2(dbSource.data(), dbDestination.data());
QStringList changes2 = merger2.merge();
QVERIFY(changes2.isEmpty());
Merger merger3(dbSource2.data(), dbDestination2.data());
merger3.merge();
// Target is newer, no data is merged
QVERIFY(!dbDestination2->metadata()->customData()->isEmpty());
QVERIFY(!dbDestination2->metadata()->customData()->contains("key1"));
QVERIFY(!dbDestination2->metadata()->customData()->contains("key2"));
QVERIFY(!dbDestination2->metadata()->customData()->contains("Browser"));
QVERIFY(dbDestination2->metadata()->customData()->contains("notToBeDeleted"));
QCOMPARE(dbDestination2->metadata()->customData()->value("key3"),
QString("oldValue")); // Old value should not be replaced
}
void TestMerge::testDeletedEntry()
{
QScopedPointer<Database> dbDestination(createTestDatabase());
QScopedPointer<Database> dbSource(
createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
m_clock->advanceSecond(1);
QPointer<Entry> entry1SourceInitial = dbSource->rootGroup()->findEntryByPath("entry1");
QVERIFY(entry1SourceInitial != nullptr);
QUuid entry1Uuid = entry1SourceInitial->uuid();
delete entry1SourceInitial;
QVERIFY(dbSource->containsDeletedObject(entry1Uuid));
m_clock->advanceSecond(1);
QPointer<Entry> entry2DestinationInitial = dbDestination->rootGroup()->findEntryByPath("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()->findEntryByPath("entry1");
QVERIFY(entry1DestinationMerged);
QVERIFY(!dbDestination->containsDeletedObject(entry1Uuid));
QPointer<Entry> entry2DestinationMerged = dbDestination->rootGroup()->findEntryByPath("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);
auto 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()->findEntryByPath("entry1");
QVERIFY(entry1SourceInitial != nullptr);
QPointer<Entry> entry2SourceInitial = dbSource->rootGroup()->findEntryByPath("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()->findEntryByPath("entry1");
QVERIFY(entry1DestinationMerged);
QPointer<Entry> entry2DestinationMerged = dbDestination->rootGroup()->findEntryByPath("entry2");
QVERIFY(entry2DestinationMerged);
QPointer<Entry> entry3DestinationMerged = dbDestination->rootGroup()->findEntryByPath("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()->findEntryByPath("entry1");
QVERIFY(entry1DestinationInitial != nullptr);
QUuid entry1Uuid = entry1DestinationInitial->uuid();
delete entry1DestinationInitial;
QVERIFY(dbDestination->containsDeletedObject(entry1Uuid));
m_clock->advanceSecond(1);
QPointer<Entry> entry2SourceInitial = dbSource->rootGroup()->findEntryByPath("entry2");
QVERIFY(entry2SourceInitial != nullptr);
QUuid entry2Uuid = entry2SourceInitial->uuid();
delete entry2SourceInitial;
QVERIFY(dbSource->containsDeletedObject(entry2Uuid));
m_clock->advanceSecond(1);
QPointer<Entry> entry1SourceInitial = dbSource->rootGroup()->findEntryByPath("entry1");
QVERIFY(entry1SourceInitial != nullptr);
entry1SourceInitial->setNotes("Updated");
QPointer<Entry> entry2DestinationInitial = dbDestination->rootGroup()->findEntryByPath("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()->findEntryByPath("entry1");
QVERIFY(entry1DestinationMerged);
QVERIFY(entry1DestinationMerged->notes() == "Updated");
QPointer<Entry> entry2DestinationMerged = dbDestination->rootGroup()->findEntryByPath("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");
}
/**
* If the group is updated in the source database, and the
* destination database after, the group should remain the
* same.
*/
void TestMerge::testResolveGroupConflictOlder()
{
QScopedPointer<Database> dbDestination(createTestDatabase());
QScopedPointer<Database> dbSource(
createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
// sanity check
QPointer<Group> groupSourceInitial = dbSource->rootGroup()->findChildByName("group1");
QVERIFY(groupSourceInitial != nullptr);
// Make sure the two changes have a different timestamp.
m_clock->advanceSecond(1);
groupSourceInitial->setName("group1 updated in source");
// Make sure the two changes have a different timestamp.
m_clock->advanceSecond(1);
QPointer<Group> groupDestinationUpdated = dbDestination->rootGroup()->findChildByName("group1");
groupDestinationUpdated->setName("group1 updated in destination");
m_clock->advanceSecond(1);
Merger merger(dbSource.data(), dbDestination.data());
merger.merge();
// sanity check
QPointer<Group> groupDestinationMerged =
dbDestination->rootGroup()->findChildByName("group1 updated in destination");
QVERIFY(groupDestinationMerged != nullptr);
}
void TestMerge::testMergeNotModified()
{
QScopedPointer<Database> dbDestination(createTestDatabase());
QScopedPointer<Database> dbSource(
createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
QSignalSpy modifiedSignalSpy(dbDestination.data(), SIGNAL(modified()));
Merger merger(dbSource.data(), dbDestination.data());
merger.merge();
QTRY_VERIFY(modifiedSignalSpy.empty());
}
void TestMerge::testMergeModified()
{
QScopedPointer<Database> dbDestination(createTestDatabase());
QScopedPointer<Database> dbSource(
createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
QSignalSpy modifiedSignalSpy(dbDestination.data(), SIGNAL(modified()));
// Make sure the two changes have a different timestamp.
QTest::qSleep(1);
Entry* entry = dbSource->rootGroup()->findEntryByPath("entry1");
entry->beginUpdate();
entry->setTitle("new title");
entry->endUpdate();
Merger merger(dbSource.data(), dbDestination.data());
merger.merge();
QTRY_VERIFY(!modifiedSignalSpy.empty());
}
Database* TestMerge::createTestDatabase()
{
auto db = new Database();
auto group1 = new Group();
group1->setName("group1");
group1->setUuid(QUuid::createUuid());
auto group2 = new Group();
group2->setName("group2");
group2->setUuid(QUuid::createUuid());
auto entry1 = new Entry();
entry1->setUuid(QUuid::createUuid());
auto entry2 = new Entry();
entry2->setUuid(QUuid::createUuid());
m_clock->advanceYear(1);
// Give Entry 1 a history
entry1->beginUpdate();
entry1->setGroup(group1);
entry1->setTitle("entry1");
entry1->endUpdate();
// Give Entry 2 a history
entry2->beginUpdate();
entry2->setGroup(group1);
entry2->setTitle("entry2");
entry2->endUpdate();
group1->setParent(db->rootGroup());
group2->setParent(db->rootGroup());
return db;
}
Database* TestMerge::createTestDatabaseStructureClone(Database* source, int entryFlags, int groupFlags)
{
auto db = new Database();
auto oldGroup = db->setRootGroup(source->rootGroup()->clone(static_cast<Entry::CloneFlag>(entryFlags),
static_cast<Group::CloneFlag>(groupFlags)));
delete oldGroup;
return db;
}