keepassxc

Форк
0
/
TestMerge.cpp 
1499 строк · 65.6 Кб
1
/*
2
 *  Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
3
 *
4
 *  This program is free software: you can redistribute it and/or modify
5
 *  it under the terms of the GNU General Public License as published by
6
 *  the Free Software Foundation, either version 2 or (at your option)
7
 *  version 3 of the License.
8
 *
9
 *  This program is distributed in the hope that it will be useful,
10
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 *  GNU General Public License for more details.
13
 *
14
 *  You should have received a copy of the GNU General Public License
15
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
 */
17

18
#include "TestMerge.h"
19
#include "mock/MockClock.h"
20

21
#include "core/Merger.h"
22
#include "core/Metadata.h"
23
#include "crypto/Crypto.h"
24

25
#include <QSignalSpy>
26
#include <QTest>
27

28
QTEST_GUILESS_MAIN(TestMerge)
29

30
namespace
31
{
32
    MockClock* m_clock = nullptr;
33
} // namespace
34

35
void TestMerge::initTestCase()
36
{
37
    qRegisterMetaType<Entry*>("Entry*");
38
    qRegisterMetaType<Group*>("Group*");
39
    QVERIFY(Crypto::init());
40
}
41

42
void TestMerge::init()
43
{
44
    Q_ASSERT(m_clock == nullptr);
45
    m_clock = new MockClock(2010, 5, 5, 10, 30, 10);
46
    MockClock::setup(m_clock);
47
}
48

49
void TestMerge::cleanup()
50
{
51
    MockClock::teardown();
52
    m_clock = nullptr;
53
}
54

55
/**
56
 * Merge an existing database into a new one.
57
 * All the entries of the existing should end
58
 * up in the new one.
59
 */
60
void TestMerge::testMergeIntoNew()
61
{
62
    QScopedPointer<Database> dbSource(createTestDatabase());
63
    QScopedPointer<Database> dbDestination(new Database());
64

65
    Merger merger(dbSource.data(), dbDestination.data());
66
    merger.merge();
67

68
    QCOMPARE(dbDestination->rootGroup()->children().size(), 2);
69
    QCOMPARE(dbDestination->rootGroup()->children().at(0)->entries().size(), 2);
70
    // Test for retention of history
71
    QCOMPARE(dbDestination->rootGroup()->children().at(0)->entries().at(0)->historyItems().isEmpty(), false);
72
}
73

74
/**
75
 * Merging when no changes occurred should not
76
 * have any side effect.
77
 */
78
void TestMerge::testMergeNoChanges()
79
{
80
    QScopedPointer<Database> dbDestination(createTestDatabase());
81
    QScopedPointer<Database> dbSource(
82
        createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
83

84
    QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
85
    QCOMPARE(dbSource->rootGroup()->entriesRecursive().size(), 2);
86

87
    m_clock->advanceSecond(1);
88

89
    Merger merger1(dbSource.data(), dbDestination.data());
90
    auto changes = merger1.merge();
91

92
    QVERIFY(changes.isEmpty());
93
    QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
94
    QCOMPARE(dbSource->rootGroup()->entriesRecursive().size(), 2);
95

96
    m_clock->advanceSecond(1);
97

98
    Merger merger2(dbSource.data(), dbDestination.data());
99
    changes = merger2.merge();
100

101
    QVERIFY(changes.isEmpty());
102
    QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
103
    QCOMPARE(dbSource->rootGroup()->entriesRecursive().size(), 2);
104
}
105

106
/**
107
 * Merging without database custom data (used by imports and KeeShare)
108
 */
109
void TestMerge::testMergeCustomData()
110
{
111
    QScopedPointer<Database> dbDestination(createTestDatabase());
112
    QScopedPointer<Database> dbSource(
113
        createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
114

115
    QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
116
    QCOMPARE(dbSource->rootGroup()->entriesRecursive().size(), 2);
117

118
    dbDestination->metadata()->customData()->set("TEST_CUSTOM_DATA", "OLD TESTING");
119

120
    m_clock->advanceSecond(1);
121

122
    dbSource->metadata()->customData()->set("TEST_CUSTOM_DATA", "TESTING");
123

124
    // First check that the custom data is not merged when skipped
125
    Merger merger1(dbSource.data(), dbDestination.data());
126
    merger1.setSkipDatabaseCustomData(true);
127
    auto changes = merger1.merge();
128

129
    QVERIFY(changes.isEmpty());
130
    QCOMPARE(dbDestination->metadata()->customData()->value("TEST_CUSTOM_DATA"), QString("OLD TESTING"));
131

132
    // Second check that the custom data is merged otherwise
133
    Merger merger2(dbSource.data(), dbDestination.data());
134
    changes = merger2.merge();
135

136
    QCOMPARE(changes.size(), 1);
137
    QCOMPARE(dbDestination->metadata()->customData()->value("TEST_CUSTOM_DATA"), QString("TESTING"));
138
}
139

140
/**
141
 * If the entry is updated in the source database, the update
142
 * should propagate in the destination database.
143
 */
144
void TestMerge::testResolveConflictNewer()
145
{
146
    QScopedPointer<Database> dbDestination(createTestDatabase());
147
    QScopedPointer<Database> dbSource(
148
        createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
149

150
    // sanity check
151
    QPointer<Group> groupSourceInitial = dbSource->rootGroup()->findChildByName("group1");
152
    QVERIFY(groupSourceInitial != nullptr);
153
    QCOMPARE(groupSourceInitial->entries().size(), 2);
154

155
    QPointer<Group> groupDestinationInitial = dbSource->rootGroup()->findChildByName("group1");
156
    QVERIFY(groupDestinationInitial != nullptr);
157
    QCOMPARE(groupDestinationInitial->entries().size(), 2);
158

159
    QPointer<Entry> entrySourceInitial = dbSource->rootGroup()->findEntryByPath("entry1");
160
    QVERIFY(entrySourceInitial != nullptr);
161
    QVERIFY(entrySourceInitial->group() == groupSourceInitial);
162

163
    const TimeInfo entrySourceInitialTimeInfo = entrySourceInitial->timeInfo();
164
    const TimeInfo groupSourceInitialTimeInfo = groupSourceInitial->timeInfo();
165
    const TimeInfo groupDestinationInitialTimeInfo = groupDestinationInitial->timeInfo();
166

167
    // Make sure the two changes have a different timestamp.
168
    m_clock->advanceSecond(1);
169
    // make this entry newer than in destination db
170
    entrySourceInitial->beginUpdate();
171
    entrySourceInitial->setPassword("password");
172
    entrySourceInitial->endUpdate();
173

174
    const TimeInfo entrySourceUpdatedTimeInfo = entrySourceInitial->timeInfo();
175
    const TimeInfo groupSourceUpdatedTimeInfo = groupSourceInitial->timeInfo();
176

177
    QVERIFY(entrySourceInitialTimeInfo != entrySourceUpdatedTimeInfo);
178
    QVERIFY(groupSourceInitialTimeInfo == groupSourceUpdatedTimeInfo);
179
    QVERIFY(groupSourceInitialTimeInfo == groupDestinationInitialTimeInfo);
180

181
    // Make sure the merge changes have a different timestamp.
182
    m_clock->advanceSecond(1);
183

184
    Merger merger(dbSource.data(), dbDestination.data());
185
    merger.merge();
186

187
    // sanity check
188
    QPointer<Group> groupDestinationMerged = dbDestination->rootGroup()->findChildByName("group1");
189
    QVERIFY(groupDestinationMerged != nullptr);
190
    QCOMPARE(groupDestinationMerged->entries().size(), 2);
191
    QCOMPARE(groupDestinationMerged->timeInfo(), groupDestinationInitialTimeInfo);
192

193
    QPointer<Entry> entryDestinationMerged = dbDestination->rootGroup()->findEntryByPath("entry1");
194
    QVERIFY(entryDestinationMerged != nullptr);
195
    QVERIFY(entryDestinationMerged->group() != nullptr);
196
    QCOMPARE(entryDestinationMerged->password(), QString("password"));
197
    QCOMPARE(entryDestinationMerged->timeInfo(), entrySourceUpdatedTimeInfo);
198

199
    // When updating an entry, it should not end up in the
200
    // deleted objects.
201
    for (DeletedObject deletedObject : dbDestination->deletedObjects()) {
202
        QVERIFY(deletedObject.uuid != entryDestinationMerged->uuid());
203
    }
204
}
205

206
/**
207
 * If the entry is updated in the source database, and the
208
 * destination database after, the entry should remain the
209
 * same.
210
 */
211
void TestMerge::testResolveConflictExisting()
212
{
213
    QScopedPointer<Database> dbDestination(createTestDatabase());
214
    QScopedPointer<Database> dbSource(
215
        createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
216

217
    // sanity check
218
    QPointer<Group> groupSourceInitial = dbSource->rootGroup()->findChildByName("group1");
219
    QVERIFY(groupSourceInitial != nullptr);
220
    QCOMPARE(groupSourceInitial->entries().size(), 2);
221

222
    QPointer<Group> groupDestinationInitial = dbDestination->rootGroup()->findChildByName("group1");
223
    QVERIFY(groupDestinationInitial != nullptr);
224
    QCOMPARE(groupSourceInitial->entries().size(), 2);
225

226
    QPointer<Entry> entrySourceInitial = dbSource->rootGroup()->findEntryByPath("entry1");
227
    QVERIFY(entrySourceInitial != nullptr);
228
    QVERIFY(entrySourceInitial->group() == groupSourceInitial);
229

230
    const TimeInfo entrySourceInitialTimeInfo = entrySourceInitial->timeInfo();
231
    const TimeInfo groupSourceInitialTimeInfo = groupSourceInitial->timeInfo();
232
    const TimeInfo groupDestinationInitialTimeInfo = groupDestinationInitial->timeInfo();
233

234
    // Make sure the two changes have a different timestamp.
235
    m_clock->advanceSecond(1);
236
    // make this entry older than in destination db
237
    entrySourceInitial->beginUpdate();
238
    entrySourceInitial->setPassword("password1");
239
    entrySourceInitial->endUpdate();
240

241
    const TimeInfo entrySourceUpdatedOlderTimeInfo = entrySourceInitial->timeInfo();
242
    const TimeInfo groupSourceUpdatedOlderTimeInfo = groupSourceInitial->timeInfo();
243

244
    QPointer<Group> groupDestinationUpdated = dbDestination->rootGroup()->findChildByName("group1");
245
    QVERIFY(groupDestinationUpdated != nullptr);
246
    QCOMPARE(groupDestinationUpdated->entries().size(), 2);
247
    QPointer<Entry> entryDestinationUpdated = dbDestination->rootGroup()->findEntryByPath("entry1");
248
    QVERIFY(entryDestinationUpdated != nullptr);
249
    QVERIFY(entryDestinationUpdated->group() == groupDestinationUpdated);
250

251
    // Make sure the two changes have a different timestamp.
252
    m_clock->advanceSecond(1);
253
    // make this entry newer than in source db
254
    entryDestinationUpdated->beginUpdate();
255
    entryDestinationUpdated->setPassword("password2");
256
    entryDestinationUpdated->endUpdate();
257

258
    const TimeInfo entryDestinationUpdatedNewerTimeInfo = entryDestinationUpdated->timeInfo();
259
    const TimeInfo groupDestinationUpdatedNewerTimeInfo = groupDestinationUpdated->timeInfo();
260
    QVERIFY(entrySourceUpdatedOlderTimeInfo != entrySourceInitialTimeInfo);
261
    QVERIFY(entrySourceUpdatedOlderTimeInfo != entryDestinationUpdatedNewerTimeInfo);
262
    QVERIFY(groupSourceInitialTimeInfo == groupSourceUpdatedOlderTimeInfo);
263
    QVERIFY(groupDestinationInitialTimeInfo == groupDestinationUpdatedNewerTimeInfo);
264
    QVERIFY(groupSourceInitialTimeInfo == groupDestinationInitialTimeInfo);
265

266
    // Make sure the merge changes have a different timestamp.
267
    m_clock->advanceSecond(1);
268

269
    Merger merger(dbSource.data(), dbDestination.data());
270
    merger.merge();
271

272
    // sanity check
273
    QPointer<Group> groupDestinationMerged = dbDestination->rootGroup()->findChildByName("group1");
274
    QVERIFY(groupDestinationMerged != nullptr);
275
    QCOMPARE(groupDestinationMerged->entries().size(), 2);
276
    QCOMPARE(groupDestinationMerged->timeInfo(), groupDestinationUpdatedNewerTimeInfo);
277

278
    QPointer<Entry> entryDestinationMerged = dbDestination->rootGroup()->findEntryByPath("entry1");
279
    QVERIFY(entryDestinationMerged != nullptr);
280
    QCOMPARE(entryDestinationMerged->password(), QString("password2"));
281
    QCOMPARE(entryDestinationMerged->timeInfo(), entryDestinationUpdatedNewerTimeInfo);
282

283
    // When updating an entry, it should not end up in the
284
    // deleted objects.
285
    for (DeletedObject deletedObject : dbDestination->deletedObjects()) {
286
        QVERIFY(deletedObject.uuid != entryDestinationMerged->uuid());
287
    }
288
}
289

290
void TestMerge::testResolveConflictTemplate(
291
    int mergeMode,
292
    std::function<void(Database*, const QMap<const char*, QDateTime>&)> verification)
293
{
294
    QMap<const char*, QDateTime> timestamps;
295
    timestamps["initialTime"] = m_clock->currentDateTimeUtc();
296
    QScopedPointer<Database> dbDestination(createTestDatabase());
297

298
    auto deletedEntry1 = new Entry();
299
    deletedEntry1->setUuid(QUuid::createUuid());
300

301
    deletedEntry1->beginUpdate();
302
    deletedEntry1->setGroup(dbDestination->rootGroup());
303
    deletedEntry1->setTitle("deletedDestination");
304
    deletedEntry1->endUpdate();
305

306
    auto deletedEntry2 = new Entry();
307
    deletedEntry2->setUuid(QUuid::createUuid());
308

309
    deletedEntry2->beginUpdate();
310
    deletedEntry2->setGroup(dbDestination->rootGroup());
311
    deletedEntry2->setTitle("deletedSource");
312
    deletedEntry2->endUpdate();
313

314
    QScopedPointer<Database> dbSource(
315
        createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneIncludeHistory, Group::CloneIncludeEntries));
316

317
    timestamps["oldestCommonHistoryTime"] = m_clock->currentDateTimeUtc();
318

319
    // sanity check
320
    QCOMPARE(dbDestination->rootGroup()->children().at(0)->entries().size(), 2);
321
    QCOMPARE(dbDestination->rootGroup()->children().at(0)->entries().at(0)->historyItems().count(), 1);
322
    QCOMPARE(dbDestination->rootGroup()->children().at(0)->entries().at(1)->historyItems().count(), 1);
323
    QCOMPARE(dbSource->rootGroup()->children().at(0)->entries().size(), 2);
324
    QCOMPARE(dbSource->rootGroup()->children().at(0)->entries().at(0)->historyItems().count(), 1);
325
    QCOMPARE(dbSource->rootGroup()->children().at(0)->entries().at(1)->historyItems().count(), 1);
326

327
    // simulate some work in the dbs (manipulate the history)
328
    QPointer<Entry> destinationEntry1 = dbDestination->rootGroup()->children().at(0)->entries().at(0);
329
    QPointer<Entry> destinationEntry2 = dbDestination->rootGroup()->children().at(0)->entries().at(1);
330
    QPointer<Entry> sourceEntry1 = dbSource->rootGroup()->children().at(0)->entries().at(0);
331
    QPointer<Entry> sourceEntry2 = dbSource->rootGroup()->children().at(0)->entries().at(1);
332

333
    timestamps["newestCommonHistoryTime"] = m_clock->advanceMinute(1);
334

335
    destinationEntry1->beginUpdate();
336
    destinationEntry1->setNotes("1 Common");
337
    destinationEntry1->endUpdate();
338
    destinationEntry2->beginUpdate();
339
    destinationEntry2->setNotes("1 Common");
340
    destinationEntry2->endUpdate();
341
    sourceEntry1->beginUpdate();
342
    sourceEntry1->setNotes("1 Common");
343
    sourceEntry1->endUpdate();
344
    sourceEntry2->beginUpdate();
345
    sourceEntry2->setNotes("1 Common");
346
    sourceEntry2->endUpdate();
347

348
    timestamps["oldestDivergingHistoryTime"] = m_clock->advanceSecond(1);
349

350
    destinationEntry2->beginUpdate();
351
    destinationEntry2->setNotes("2 Destination");
352
    destinationEntry2->endUpdate();
353
    sourceEntry1->beginUpdate();
354
    sourceEntry1->setNotes("2 Source");
355
    sourceEntry1->endUpdate();
356

357
    timestamps["newestDivergingHistoryTime"] = m_clock->advanceHour(1);
358

359
    destinationEntry1->beginUpdate();
360
    destinationEntry1->setNotes("3 Destination");
361
    destinationEntry1->endUpdate();
362
    sourceEntry2->beginUpdate();
363
    sourceEntry2->setNotes("3 Source");
364
    sourceEntry2->endUpdate();
365

366
    // sanity check
367
    QCOMPARE(dbDestination->rootGroup()->children().at(0)->entries().at(0)->historyItems().count(), 3);
368
    QCOMPARE(dbDestination->rootGroup()->children().at(0)->entries().at(1)->historyItems().count(), 3);
369
    QCOMPARE(dbSource->rootGroup()->children().at(0)->entries().at(0)->historyItems().count(), 3);
370
    QCOMPARE(dbSource->rootGroup()->children().at(0)->entries().at(1)->historyItems().count(), 3);
371

372
    m_clock->advanceMinute(1);
373

374
    QPointer<Entry> deletedEntryDestination = dbDestination->rootGroup()->findEntryByPath("deletedDestination");
375
    dbDestination->recycleEntry(deletedEntryDestination);
376
    QPointer<Entry> deletedEntrySource = dbSource->rootGroup()->findEntryByPath("deletedSource");
377
    dbSource->recycleEntry(deletedEntrySource);
378

379
    m_clock->advanceMinute(1);
380

381
    auto destinationEntrySingle = new Entry();
382
    destinationEntrySingle->setUuid(QUuid::createUuid());
383

384
    destinationEntrySingle->beginUpdate();
385
    destinationEntrySingle->setGroup(dbDestination->rootGroup()->children().at(1));
386
    destinationEntrySingle->setTitle("entryDestination");
387
    destinationEntrySingle->endUpdate();
388

389
    auto sourceEntrySingle = new Entry();
390
    sourceEntrySingle->setUuid(QUuid::createUuid());
391

392
    sourceEntrySingle->beginUpdate();
393
    sourceEntrySingle->setGroup(dbSource->rootGroup()->children().at(1));
394
    sourceEntrySingle->setTitle("entrySource");
395
    sourceEntrySingle->endUpdate();
396

397
    dbDestination->rootGroup()->setMergeMode(static_cast<Group::MergeMode>(mergeMode));
398

399
    // Make sure the merge changes have a different timestamp.
400
    timestamps["mergeTime"] = m_clock->advanceSecond(1);
401

402
    Merger merger(dbSource.data(), dbDestination.data());
403
    merger.merge();
404

405
    QPointer<Group> mergedRootGroup = dbDestination->rootGroup();
406
    QCOMPARE(mergedRootGroup->entries().size(), 0);
407
    // Both databases contain their own generated recycleBin - just one is considered a real recycleBin, the other
408
    // exists as normal group, therefore only one entry is considered deleted
409
    QCOMPARE(dbDestination->metadata()->recycleBin()->entries().size(), 1);
410
    QPointer<Group> mergedGroup1 = mergedRootGroup->children().at(0);
411
    QPointer<Group> mergedGroup2 = mergedRootGroup->children().at(1);
412
    QVERIFY(mergedGroup1);
413
    QVERIFY(mergedGroup2);
414
    QCOMPARE(mergedGroup2->entries().size(), 2);
415
    QVERIFY(mergedGroup1->entries().at(0));
416
    QVERIFY(mergedGroup1->entries().at(1));
417

418
    verification(dbDestination.data(), timestamps);
419

420
    QVERIFY(dbDestination->rootGroup()->findEntryByPath("entryDestination"));
421
    QVERIFY(dbDestination->rootGroup()->findEntryByPath("entrySource"));
422
}
423

424
void TestMerge::testDeletionConflictTemplate(int mergeMode,
425
                                             std::function<void(Database*, const QMap<QString, QUuid>&)> verification)
426
{
427
    QMap<QString, QUuid> identifiers;
428
    m_clock->currentDateTimeUtc();
429
    QScopedPointer<Database> dbDestination(createTestDatabase());
430

431
    // scenarios:
432
    //   entry directly deleted in source before updated in target
433
    //   entry directly deleted in source after updated in target
434
    //   entry directly deleted in target before updated in source
435
    //   entry directly deleted in target after updated in source
436

437
    //   entry indirectly deleted in source before updated in target
438
    //   entry indirectly deleted in source after updated in target
439
    //   entry indirectly deleted in target before updated in source
440
    //   entry indirectly deleted in target after updated in source
441

442
    auto createGroup = [&](const char* name, Group* parent) {
443
        auto group = new Group();
444
        group->setUuid(QUuid::createUuid());
445
        group->setName(name);
446
        group->setParent(parent, 0);
447
        identifiers[group->name()] = group->uuid();
448
        return group;
449
    };
450
    auto createEntry = [&](const char* title, Group* parent) {
451
        auto entry = new Entry();
452
        entry->setUuid(QUuid::createUuid());
453
        entry->setTitle(title);
454
        entry->setGroup(parent);
455
        identifiers[entry->title()] = entry->uuid();
456
        return entry;
457
    };
458
    auto changeEntry = [](Entry* entry) {
459
        entry->beginUpdate();
460
        entry->setNotes("Change");
461
        entry->endUpdate();
462
    };
463

464
    Group* directlyDeletedEntryGroup = createGroup("DirectlyDeletedEntries", dbDestination->rootGroup());
465
    createEntry("EntryDeletedInSourceBeforeChangedInTarget", directlyDeletedEntryGroup);
466
    createEntry("EntryDeletedInSourceAfterChangedInTarget", directlyDeletedEntryGroup);
467
    createEntry("EntryDeletedInTargetBeforeChangedInSource", directlyDeletedEntryGroup);
468
    createEntry("EntryDeletedInTargetAfterChangedInSource", directlyDeletedEntryGroup);
469

470
    Group* groupDeletedInSourceBeforeEntryUpdatedInTarget =
471
        createGroup("GroupDeletedInSourceBeforeEntryUpdatedInTarget", dbDestination->rootGroup());
472
    createEntry("EntryDeletedInSourceBeforeEntryUpdatedInTarget", groupDeletedInSourceBeforeEntryUpdatedInTarget);
473

474
    Group* groupDeletedInSourceAfterEntryUpdatedInTarget =
475
        createGroup("GroupDeletedInSourceAfterEntryUpdatedInTarget", dbDestination->rootGroup());
476
    createEntry("EntryDeletedInSourceAfterEntryUpdatedInTarget", groupDeletedInSourceAfterEntryUpdatedInTarget);
477

478
    Group* groupDeletedInTargetBeforeEntryUpdatedInSource =
479
        createGroup("GroupDeletedInTargetBeforeEntryUpdatedInSource", dbDestination->rootGroup());
480
    createEntry("EntryDeletedInTargetBeforeEntryUpdatedInSource", groupDeletedInTargetBeforeEntryUpdatedInSource);
481

482
    Group* groupDeletedInTargetAfterEntryUpdatedInSource =
483
        createGroup("GroupDeletedInTargetAfterEntryUpdatedInSource", dbDestination->rootGroup());
484
    createEntry("EntryDeletedInTargetAfterEntryUpdatedInSource", groupDeletedInTargetAfterEntryUpdatedInSource);
485

486
    QScopedPointer<Database> dbSource(
487
        createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneIncludeHistory, Group::CloneIncludeEntries));
488

489
    QPointer<Entry> sourceEntryDeletedInSourceBeforeChangedInTarget =
490
        dbSource->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInSourceBeforeChangedInTarget"]);
491
    QPointer<Entry> targetEntryDeletedInSourceBeforeChangedInTarget =
492
        dbDestination->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInSourceBeforeChangedInTarget"]);
493

494
    QPointer<Entry> sourceEntryDeletedInSourceAfterChangedInTarget =
495
        dbSource->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInSourceAfterChangedInTarget"]);
496
    QPointer<Entry> targetEntryDeletedInSourceAfterChangedInTarget =
497
        dbDestination->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInSourceAfterChangedInTarget"]);
498

499
    QPointer<Entry> sourceEntryDeletedInTargetBeforeChangedInSource =
500
        dbSource->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInTargetBeforeChangedInSource"]);
501
    QPointer<Entry> targetEntryDeletedInTargetBeforeChangedInSource =
502
        dbDestination->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInTargetBeforeChangedInSource"]);
503

504
    QPointer<Entry> sourceEntryDeletedInTargetAfterChangedInSource =
505
        dbSource->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInTargetAfterChangedInSource"]);
506
    QPointer<Entry> targetEntryDeletedInTargetAfterChangedInSource =
507
        dbDestination->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInTargetAfterChangedInSource"]);
508

509
    QPointer<Group> sourceGroupDeletedInSourceBeforeEntryUpdatedInTarget =
510
        dbSource->rootGroup()->findGroupByUuid(identifiers["GroupDeletedInSourceBeforeEntryUpdatedInTarget"]);
511
    QPointer<Entry> targetEntryDeletedInSourceBeforeEntryUpdatedInTarget =
512
        dbDestination->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInSourceBeforeEntryUpdatedInTarget"]);
513

514
    QPointer<Group> sourceGroupDeletedInSourceAfterEntryUpdatedInTarget =
515
        dbSource->rootGroup()->findGroupByUuid(identifiers["GroupDeletedInSourceAfterEntryUpdatedInTarget"]);
516
    QPointer<Entry> targetEntryDeletedInSourceAfterEntryUpdatedInTarget =
517
        dbDestination->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInSourceAfterEntryUpdatedInTarget"]);
518

519
    QPointer<Group> targetGroupDeletedInTargetBeforeEntryUpdatedInSource =
520
        dbDestination->rootGroup()->findGroupByUuid(identifiers["GroupDeletedInTargetBeforeEntryUpdatedInSource"]);
521
    QPointer<Entry> sourceEntryDeletedInTargetBeforeEntryUpdatedInSource =
522
        dbSource->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInTargetBeforeEntryUpdatedInSource"]);
523

524
    QPointer<Group> targetGroupDeletedInTargetAfterEntryUpdatedInSource =
525
        dbDestination->rootGroup()->findGroupByUuid(identifiers["GroupDeletedInTargetAfterEntryUpdatedInSource"]);
526
    QPointer<Entry> sourceEntryDeletedInTargetAfterEntryUpdatedInSoruce =
527
        dbSource->rootGroup()->findEntryByUuid(identifiers["EntryDeletedInTargetAfterEntryUpdatedInSource"]);
528

529
    // simulate some work in the dbs (manipulate the history)
530
    m_clock->advanceMinute(1);
531

532
    delete sourceEntryDeletedInSourceBeforeChangedInTarget.data();
533
    changeEntry(targetEntryDeletedInSourceAfterChangedInTarget);
534
    delete targetEntryDeletedInTargetBeforeChangedInSource.data();
535
    changeEntry(sourceEntryDeletedInTargetAfterChangedInSource);
536

537
    delete sourceGroupDeletedInSourceBeforeEntryUpdatedInTarget.data();
538
    changeEntry(targetEntryDeletedInSourceAfterEntryUpdatedInTarget);
539
    delete targetGroupDeletedInTargetBeforeEntryUpdatedInSource.data();
540
    changeEntry(sourceEntryDeletedInTargetAfterEntryUpdatedInSoruce);
541

542
    m_clock->advanceMinute(1);
543

544
    changeEntry(targetEntryDeletedInSourceBeforeChangedInTarget);
545
    delete sourceEntryDeletedInSourceAfterChangedInTarget.data();
546
    changeEntry(sourceEntryDeletedInTargetBeforeChangedInSource);
547
    delete targetEntryDeletedInTargetAfterChangedInSource.data();
548

549
    changeEntry(targetEntryDeletedInSourceBeforeEntryUpdatedInTarget);
550
    delete sourceGroupDeletedInSourceAfterEntryUpdatedInTarget.data();
551
    changeEntry(sourceEntryDeletedInTargetBeforeEntryUpdatedInSource);
552
    delete targetGroupDeletedInTargetAfterEntryUpdatedInSource.data();
553
    m_clock->advanceMinute(1);
554

555
    dbDestination->rootGroup()->setMergeMode(static_cast<Group::MergeMode>(mergeMode));
556

557
    Merger merger(dbSource.data(), dbDestination.data());
558
    merger.merge();
559

560
    verification(dbDestination.data(), identifiers);
561
}
562

563
void TestMerge::assertDeletionNewerOnly(Database* db, const QMap<QString, QUuid>& identifiers)
564
{
565
    QPointer<Group> mergedRootGroup = db->rootGroup();
566
    // newer change in target prevents deletion
567
    QVERIFY(mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInSourceBeforeChangedInTarget"]));
568
    QVERIFY(!db->containsDeletedObject(identifiers["EntryDeletedInSourceBeforeChangedInTarget"]));
569
    // newer deletion in source forces deletion
570
    QVERIFY(!mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInSourceAfterChangedInTarget"]));
571
    QVERIFY(db->containsDeletedObject(identifiers["EntryDeletedInSourceAfterChangedInTarget"]));
572
    // newer change in source prevents deletion
573
    QVERIFY(mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInTargetBeforeChangedInSource"]));
574
    QVERIFY(!db->containsDeletedObject(identifiers["EntryDeletedInTargetBeforeChangedInSource"]));
575
    // newer deletion in target forces deletion
576
    QVERIFY(!mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInTargetAfterChangedInSource"]));
577
    QVERIFY(db->containsDeletedObject(identifiers["EntryDeletedInTargetAfterChangedInSource"]));
578
    // newer change in target prevents deletion
579
    QVERIFY(mergedRootGroup->findGroupByUuid(identifiers["GroupDeletedInSourceBeforeEntryUpdatedInTarget"]));
580
    QVERIFY(!db->containsDeletedObject(identifiers["GroupDeletedInSourceBeforeEntryUpdatedInTarget"]));
581
    QVERIFY(mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInSourceBeforeEntryUpdatedInTarget"]));
582
    QVERIFY(!db->containsDeletedObject(identifiers["EntryDeletedInSourceBeforeEntryUpdatedInTarget"]));
583
    // newer deletion in source forces deletion
584
    QVERIFY(!mergedRootGroup->findGroupByUuid(identifiers["GroupDeletedInSourceAfterEntryUpdatedInTarget"]));
585
    QVERIFY(db->containsDeletedObject(identifiers["GroupDeletedInSourceAfterEntryUpdatedInTarget"]));
586
    QVERIFY(!mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInSourceAfterEntryUpdatedInTarget"]));
587
    QVERIFY(db->containsDeletedObject(identifiers["EntryDeletedInSourceAfterEntryUpdatedInTarget"]));
588
    // newer change in source prevents deletion
589
    QVERIFY(mergedRootGroup->findGroupByUuid(identifiers["GroupDeletedInTargetBeforeEntryUpdatedInSource"]));
590
    QVERIFY(!db->containsDeletedObject(identifiers["GroupDeletedInTargetBeforeEntryUpdatedInSource"]));
591
    QVERIFY(mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInTargetBeforeEntryUpdatedInSource"]));
592
    QVERIFY(!db->containsDeletedObject(identifiers["EntryDeletedInTargetBeforeEntryUpdatedInSource"]));
593
    // newer deletion in target forces deletion
594
    QVERIFY(!mergedRootGroup->findGroupByUuid(identifiers["GroupDeletedInTargetAfterEntryUpdatedInSource"]));
595
    QVERIFY(db->containsDeletedObject(identifiers["GroupDeletedInTargetAfterEntryUpdatedInSource"]));
596
    QVERIFY(!mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInTargetAfterEntryUpdatedInSource"]));
597
    QVERIFY(db->containsDeletedObject(identifiers["EntryDeletedInTargetAfterEntryUpdatedInSource"]));
598
}
599

600
void TestMerge::assertDeletionLocalOnly(Database* db, const QMap<QString, QUuid>& identifiers)
601
{
602
    QPointer<Group> mergedRootGroup = db->rootGroup();
603

604
    QVERIFY(mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInSourceBeforeChangedInTarget"]));
605
    QVERIFY(!db->containsDeletedObject(identifiers["EntryDeletedInSourceBeforeChangedInTarget"]));
606

607
    QVERIFY(mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInSourceAfterChangedInTarget"]));
608
    QVERIFY(!db->containsDeletedObject(identifiers["EntryDeletedInSourceAfterChangedInTarget"]));
609

610
    // Uuids in db and deletedObjects is intended according to KeePass #1752
611
    QVERIFY(mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInTargetBeforeChangedInSource"]));
612
    QVERIFY(db->containsDeletedObject(identifiers["EntryDeletedInTargetBeforeChangedInSource"]));
613

614
    QVERIFY(mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInTargetAfterChangedInSource"]));
615
    QVERIFY(db->containsDeletedObject(identifiers["EntryDeletedInTargetAfterChangedInSource"]));
616

617
    QVERIFY(mergedRootGroup->findGroupByUuid(identifiers["GroupDeletedInSourceBeforeEntryUpdatedInTarget"]));
618
    QVERIFY(!db->containsDeletedObject(identifiers["GroupDeletedInSourceBeforeEntryUpdatedInTarget"]));
619
    QVERIFY(mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInSourceBeforeEntryUpdatedInTarget"]));
620
    QVERIFY(!db->containsDeletedObject(identifiers["EntryDeletedInSourceBeforeEntryUpdatedInTarget"]));
621

622
    QVERIFY(mergedRootGroup->findGroupByUuid(identifiers["GroupDeletedInSourceAfterEntryUpdatedInTarget"]));
623
    QVERIFY(!db->containsDeletedObject(identifiers["GroupDeletedInSourceAfterEntryUpdatedInTarget"]));
624
    QVERIFY(mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInSourceAfterEntryUpdatedInTarget"]));
625
    QVERIFY(!db->containsDeletedObject(identifiers["EntryDeletedInSourceAfterEntryUpdatedInTarget"]));
626

627
    QVERIFY(mergedRootGroup->findGroupByUuid(identifiers["GroupDeletedInTargetBeforeEntryUpdatedInSource"]));
628
    QVERIFY(db->containsDeletedObject(identifiers["GroupDeletedInTargetBeforeEntryUpdatedInSource"]));
629
    QVERIFY(mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInTargetBeforeEntryUpdatedInSource"]));
630
    QVERIFY(db->containsDeletedObject(identifiers["EntryDeletedInTargetBeforeEntryUpdatedInSource"]));
631

632
    QVERIFY(mergedRootGroup->findGroupByUuid(identifiers["GroupDeletedInTargetAfterEntryUpdatedInSource"]));
633
    QVERIFY(db->containsDeletedObject(identifiers["GroupDeletedInTargetAfterEntryUpdatedInSource"]));
634
    QVERIFY(mergedRootGroup->findEntryByUuid(identifiers["EntryDeletedInTargetAfterEntryUpdatedInSource"]));
635
    QVERIFY(db->containsDeletedObject(identifiers["EntryDeletedInTargetAfterEntryUpdatedInSource"]));
636
}
637

638
void TestMerge::assertUpdateMergedEntry1(Entry* mergedEntry1, const QMap<const char*, QDateTime>& timestamps)
639
{
640
    QCOMPARE(mergedEntry1->historyItems().count(), 4);
641
    QCOMPARE(mergedEntry1->historyItems().at(0)->notes(), QString(""));
642
    QCOMPARE(mergedEntry1->historyItems().at(0)->timeInfo().lastModificationTime(), timestamps["initialTime"]);
643
    QCOMPARE(mergedEntry1->historyItems().at(1)->notes(), QString(""));
644
    QCOMPARE(mergedEntry1->historyItems().at(1)->timeInfo().lastModificationTime(),
645
             timestamps["oldestCommonHistoryTime"]);
646
    QCOMPARE(mergedEntry1->historyItems().at(2)->notes(), QString("1 Common"));
647
    QCOMPARE(mergedEntry1->historyItems().at(2)->timeInfo().lastModificationTime(),
648
             timestamps["newestCommonHistoryTime"]);
649
    QCOMPARE(mergedEntry1->historyItems().at(3)->notes(), QString("2 Source"));
650
    QCOMPARE(mergedEntry1->historyItems().at(3)->timeInfo().lastModificationTime(),
651
             timestamps["oldestDivergingHistoryTime"]);
652
    QCOMPARE(mergedEntry1->notes(), QString("3 Destination"));
653
    QCOMPARE(mergedEntry1->timeInfo().lastModificationTime(), timestamps["newestDivergingHistoryTime"]);
654
}
655

656
void TestMerge::assertUpdateReappliedEntry2(Entry* mergedEntry2, const QMap<const char*, QDateTime>& timestamps)
657
{
658
    QCOMPARE(mergedEntry2->historyItems().count(), 5);
659
    QCOMPARE(mergedEntry2->historyItems().at(0)->notes(), QString(""));
660
    QCOMPARE(mergedEntry2->historyItems().at(0)->timeInfo().lastModificationTime(), timestamps["initialTime"]);
661
    QCOMPARE(mergedEntry2->historyItems().at(1)->notes(), QString(""));
662
    QCOMPARE(mergedEntry2->historyItems().at(1)->timeInfo().lastModificationTime(),
663
             timestamps["oldestCommonHistoryTime"]);
664
    QCOMPARE(mergedEntry2->historyItems().at(2)->notes(), QString("1 Common"));
665
    QCOMPARE(mergedEntry2->historyItems().at(2)->timeInfo().lastModificationTime(),
666
             timestamps["newestCommonHistoryTime"]);
667
    QCOMPARE(mergedEntry2->historyItems().at(3)->notes(), QString("2 Destination"));
668
    QCOMPARE(mergedEntry2->historyItems().at(3)->timeInfo().lastModificationTime(),
669
             timestamps["oldestDivergingHistoryTime"]);
670
    QCOMPARE(mergedEntry2->historyItems().at(4)->notes(), QString("3 Source"));
671
    QCOMPARE(mergedEntry2->historyItems().at(4)->timeInfo().lastModificationTime(),
672
             timestamps["newestDivergingHistoryTime"]);
673
    QCOMPARE(mergedEntry2->notes(), QString("2 Destination"));
674
    QCOMPARE(mergedEntry2->timeInfo().lastModificationTime(), timestamps["mergeTime"]);
675
}
676

677
void TestMerge::assertUpdateReappliedEntry1(Entry* mergedEntry1, const QMap<const char*, QDateTime>& timestamps)
678
{
679
    QCOMPARE(mergedEntry1->historyItems().count(), 5);
680
    QCOMPARE(mergedEntry1->historyItems().at(0)->notes(), QString(""));
681
    QCOMPARE(mergedEntry1->historyItems().at(0)->timeInfo().lastModificationTime(), timestamps["initialTime"]);
682
    QCOMPARE(mergedEntry1->historyItems().at(1)->notes(), QString(""));
683
    QCOMPARE(mergedEntry1->historyItems().at(1)->timeInfo().lastModificationTime(),
684
             timestamps["oldestCommonHistoryTime"]);
685
    QCOMPARE(mergedEntry1->historyItems().at(2)->notes(), QString("1 Common"));
686
    QCOMPARE(mergedEntry1->historyItems().at(2)->timeInfo().lastModificationTime(),
687
             timestamps["newestCommonHistoryTime"]);
688
    QCOMPARE(mergedEntry1->historyItems().at(3)->notes(), QString("2 Source"));
689
    QCOMPARE(mergedEntry1->historyItems().at(3)->timeInfo().lastModificationTime(),
690
             timestamps["oldestDivergingHistoryTime"]);
691
    QCOMPARE(mergedEntry1->historyItems().at(4)->notes(), QString("3 Destination"));
692
    QCOMPARE(mergedEntry1->historyItems().at(4)->timeInfo().lastModificationTime(),
693
             timestamps["newestDivergingHistoryTime"]);
694
    QCOMPARE(mergedEntry1->notes(), QString("2 Source"));
695
    QCOMPARE(mergedEntry1->timeInfo().lastModificationTime(), timestamps["mergeTime"]);
696
}
697

698
void TestMerge::assertUpdateMergedEntry2(Entry* mergedEntry2, const QMap<const char*, QDateTime>& timestamps)
699
{
700
    QCOMPARE(mergedEntry2->historyItems().count(), 4);
701
    QCOMPARE(mergedEntry2->historyItems().at(0)->notes(), QString(""));
702
    QCOMPARE(mergedEntry2->historyItems().at(0)->timeInfo().lastModificationTime(), timestamps["initialTime"]);
703
    QCOMPARE(mergedEntry2->historyItems().at(1)->notes(), QString(""));
704
    QCOMPARE(mergedEntry2->historyItems().at(1)->timeInfo().lastModificationTime(),
705
             timestamps["oldestCommonHistoryTime"]);
706
    QCOMPARE(mergedEntry2->historyItems().at(2)->notes(), QString("1 Common"));
707
    QCOMPARE(mergedEntry2->historyItems().at(2)->timeInfo().lastModificationTime(),
708
             timestamps["newestCommonHistoryTime"]);
709
    QCOMPARE(mergedEntry2->historyItems().at(3)->notes(), QString("2 Destination"));
710
    QCOMPARE(mergedEntry2->historyItems().at(3)->timeInfo().lastModificationTime(),
711
             timestamps["oldestDivergingHistoryTime"]);
712
    QCOMPARE(mergedEntry2->notes(), QString("3 Source"));
713
    QCOMPARE(mergedEntry2->timeInfo().lastModificationTime(), timestamps["newestDivergingHistoryTime"]);
714
}
715

716
void TestMerge::testDeletionConflictEntry_Synchronized()
717
{
718
    testDeletionConflictTemplate(Group::Synchronize, &TestMerge::assertDeletionNewerOnly);
719
}
720

721
void TestMerge::testDeletionConflictEntry_KeepNewer()
722
{
723
    testDeletionConflictTemplate(Group::KeepNewer, &TestMerge::assertDeletionLocalOnly);
724
}
725

726
/**
727
 * Tests the KeepNewer mode concerning history.
728
 */
729
void TestMerge::testResolveConflictEntry_Synchronize()
730
{
731
    testResolveConflictTemplate(Group::Synchronize, [](Database* db, const QMap<const char*, QDateTime>& timestamps) {
732
        QPointer<Group> mergedRootGroup = db->rootGroup();
733
        QPointer<Group> mergedGroup1 = mergedRootGroup->children().at(0);
734
        TestMerge::assertUpdateMergedEntry1(mergedGroup1->entries().at(0), timestamps);
735
        TestMerge::assertUpdateMergedEntry2(mergedGroup1->entries().at(1), timestamps);
736
    });
737
}
738

739
void TestMerge::testResolveConflictEntry_KeepNewer()
740
{
741
    testResolveConflictTemplate(Group::KeepNewer, [](Database* db, const QMap<const char*, QDateTime>& timestamps) {
742
        QPointer<Group> mergedRootGroup = db->rootGroup();
743
        QPointer<Group> mergedGroup1 = mergedRootGroup->children().at(0);
744
        TestMerge::assertUpdateMergedEntry1(mergedGroup1->entries().at(0), timestamps);
745
        TestMerge::assertUpdateMergedEntry2(mergedGroup1->entries().at(1), timestamps);
746
    });
747
}
748

749
/**
750
 * The location of an entry should be updated in the
751
 * destination database.
752
 */
753
void TestMerge::testMoveEntry()
754
{
755
    QScopedPointer<Database> dbDestination(createTestDatabase());
756
    QScopedPointer<Database> dbSource(
757
        createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
758

759
    QPointer<Entry> entrySourceInitial = dbSource->rootGroup()->findEntryByPath("entry1");
760
    QVERIFY(entrySourceInitial != nullptr);
761

762
    QPointer<Group> groupSourceInitial = dbSource->rootGroup()->findChildByName("group2");
763
    QVERIFY(groupSourceInitial != nullptr);
764

765
    // Make sure the two changes have a different timestamp.
766
    m_clock->advanceSecond(1);
767

768
    entrySourceInitial->setGroup(groupSourceInitial);
769
    QCOMPARE(entrySourceInitial->group()->name(), QString("group2"));
770

771
    m_clock->advanceSecond(1);
772

773
    Merger merger(dbSource.data(), dbDestination.data());
774
    merger.merge();
775

776
    QPointer<Entry> entryDestinationMerged = dbDestination->rootGroup()->findEntryByPath("entry1");
777
    QVERIFY(entryDestinationMerged != nullptr);
778
    QCOMPARE(entryDestinationMerged->group()->name(), QString("group2"));
779
    QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
780
}
781

782
/**
783
 * The location of an entry should be updated in the
784
 * destination database, but changes from the destination
785
 * database should be preserved.
786
 */
787
void TestMerge::testMoveEntryPreserveChanges()
788
{
789
    QScopedPointer<Database> dbDestination(createTestDatabase());
790
    QScopedPointer<Database> dbSource(
791
        createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
792

793
    QPointer<Entry> entrySourceInitial = dbSource->rootGroup()->findEntryByPath("entry1");
794
    QVERIFY(entrySourceInitial != nullptr);
795

796
    QPointer<Group> group2Source = dbSource->rootGroup()->findChildByName("group2");
797
    QVERIFY(group2Source != nullptr);
798

799
    m_clock->advanceSecond(1);
800

801
    entrySourceInitial->setGroup(group2Source);
802
    QCOMPARE(entrySourceInitial->group()->name(), QString("group2"));
803

804
    QPointer<Entry> entryDestinationInitial = dbDestination->rootGroup()->findEntryByPath("entry1");
805
    QVERIFY(entryDestinationInitial != nullptr);
806

807
    m_clock->advanceSecond(1);
808

809
    entryDestinationInitial->beginUpdate();
810
    entryDestinationInitial->setPassword("password");
811
    entryDestinationInitial->endUpdate();
812

813
    m_clock->advanceSecond(1);
814

815
    Merger merger(dbSource.data(), dbDestination.data());
816
    merger.merge();
817

818
    QPointer<Entry> entryDestinationMerged = dbDestination->rootGroup()->findEntryByPath("entry1");
819
    QVERIFY(entryDestinationMerged != nullptr);
820
    QCOMPARE(entryDestinationMerged->group()->name(), QString("group2"));
821
    QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
822
    QCOMPARE(entryDestinationMerged->password(), QString("password"));
823
}
824

825
void TestMerge::testCreateNewGroups()
826
{
827
    QScopedPointer<Database> dbDestination(createTestDatabase());
828
    QScopedPointer<Database> dbSource(
829
        createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
830

831
    m_clock->advanceSecond(1);
832

833
    auto groupSourceCreated = new Group();
834
    groupSourceCreated->setName("group3");
835
    groupSourceCreated->setUuid(QUuid::createUuid());
836
    groupSourceCreated->setParent(dbSource->rootGroup());
837

838
    m_clock->advanceSecond(1);
839

840
    Merger merger(dbSource.data(), dbDestination.data());
841
    merger.merge();
842

843
    QPointer<Group> groupDestinationMerged = dbDestination->rootGroup()->findChildByName("group3");
844
    QVERIFY(groupDestinationMerged != nullptr);
845
    QCOMPARE(groupDestinationMerged->name(), QString("group3"));
846
}
847

848
void TestMerge::testMoveEntryIntoNewGroup()
849
{
850
    QScopedPointer<Database> dbDestination(createTestDatabase());
851
    QScopedPointer<Database> dbSource(
852
        createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
853

854
    m_clock->advanceSecond(1);
855

856
    auto groupSourceCreated = new Group();
857
    groupSourceCreated->setName("group3");
858
    groupSourceCreated->setUuid(QUuid::createUuid());
859
    groupSourceCreated->setParent(dbSource->rootGroup());
860

861
    QPointer<Entry> entrySourceMoved = dbSource->rootGroup()->findEntryByPath("entry1");
862
    entrySourceMoved->setGroup(groupSourceCreated);
863

864
    m_clock->advanceSecond(1);
865

866
    Merger merger(dbSource.data(), dbDestination.data());
867
    merger.merge();
868

869
    QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
870

871
    QPointer<Group> groupDestinationMerged = dbDestination->rootGroup()->findChildByName("group3");
872
    QVERIFY(groupDestinationMerged != nullptr);
873
    QCOMPARE(groupDestinationMerged->name(), QString("group3"));
874
    QCOMPARE(groupDestinationMerged->entries().size(), 1);
875

876
    QPointer<Entry> entryDestinationMerged = dbDestination->rootGroup()->findEntryByPath("entry1");
877
    QVERIFY(entryDestinationMerged != nullptr);
878
    QCOMPARE(entryDestinationMerged->group()->name(), QString("group3"));
879
}
880

881
/**
882
 * Even though the entries' locations are no longer
883
 * the same, we will keep associating them.
884
 */
885
void TestMerge::testUpdateEntryDifferentLocation()
886
{
887
    QScopedPointer<Database> dbDestination(createTestDatabase());
888
    QScopedPointer<Database> dbSource(
889
        createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
890

891
    auto groupDestinationCreated = new Group();
892
    groupDestinationCreated->setName("group3");
893
    groupDestinationCreated->setUuid(QUuid::createUuid());
894
    groupDestinationCreated->setParent(dbDestination->rootGroup());
895

896
    m_clock->advanceSecond(1);
897

898
    QPointer<Entry> entryDestinationMoved = dbDestination->rootGroup()->findEntryByPath("entry1");
899
    QVERIFY(entryDestinationMoved != nullptr);
900
    entryDestinationMoved->setGroup(groupDestinationCreated);
901
    QUuid uuidBeforeSyncing = entryDestinationMoved->uuid();
902
    QDateTime destinationLocationChanged = entryDestinationMoved->timeInfo().locationChanged();
903

904
    // Change the entry in the source db.
905
    m_clock->advanceSecond(1);
906

907
    QPointer<Entry> entrySourceMoved = dbSource->rootGroup()->findEntryByPath("entry1");
908
    QVERIFY(entrySourceMoved != nullptr);
909
    entrySourceMoved->beginUpdate();
910
    entrySourceMoved->setUsername("username");
911
    entrySourceMoved->endUpdate();
912
    QDateTime sourceLocationChanged = entrySourceMoved->timeInfo().locationChanged();
913

914
    QVERIFY(destinationLocationChanged > sourceLocationChanged);
915

916
    m_clock->advanceSecond(1);
917

918
    Merger merger(dbSource.data(), dbDestination.data());
919
    merger.merge();
920

921
    QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
922

923
    QPointer<Entry> entryDestinationMerged = dbDestination->rootGroup()->findEntryByPath("entry1");
924
    QVERIFY(entryDestinationMerged != nullptr);
925
    QVERIFY(entryDestinationMerged->group() != nullptr);
926
    QCOMPARE(entryDestinationMerged->username(), QString("username"));
927
    QCOMPARE(entryDestinationMerged->group()->name(), QString("group3"));
928
    QCOMPARE(uuidBeforeSyncing, entryDestinationMerged->uuid());
929
    // default merge strategy is KeepNewer - therefore the older location is used!
930
    QCOMPARE(entryDestinationMerged->timeInfo().locationChanged(), sourceLocationChanged);
931
}
932

933
/**
934
 * Groups should be updated using the uuids.
935
 */
936
void TestMerge::testUpdateGroup()
937
{
938
    QScopedPointer<Database> dbDestination(createTestDatabase());
939
    QScopedPointer<Database> dbSource(
940
        createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
941

942
    m_clock->advanceSecond(1);
943

944
    QPointer<Group> groupSourceInitial = dbSource->rootGroup()->findChildByName("group2");
945
    groupSourceInitial->setName("group2 renamed");
946
    groupSourceInitial->setNotes("updated notes");
947
    QUuid customIconId = QUuid::createUuid();
948
    dbSource->metadata()->addCustomIcon(customIconId, QString("custom icon").toLocal8Bit());
949
    groupSourceInitial->setIcon(customIconId);
950

951
    QPointer<Entry> entrySourceInitial = dbSource->rootGroup()->findEntryByPath("entry1");
952
    QVERIFY(entrySourceInitial != nullptr);
953
    entrySourceInitial->setGroup(groupSourceInitial);
954
    entrySourceInitial->setTitle("entry1 renamed");
955
    QUuid uuidBeforeSyncing = entrySourceInitial->uuid();
956

957
    m_clock->advanceSecond(1);
958

959
    Merger merger(dbSource.data(), dbDestination.data());
960
    merger.merge();
961

962
    QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
963

964
    QPointer<Entry> entryDestinationMerged = dbDestination->rootGroup()->findEntryByPath("entry1 renamed");
965
    QVERIFY(entryDestinationMerged != nullptr);
966
    QVERIFY(entryDestinationMerged->group() != nullptr);
967
    QCOMPARE(entryDestinationMerged->group()->name(), QString("group2 renamed"));
968
    QCOMPARE(uuidBeforeSyncing, entryDestinationMerged->uuid());
969

970
    QPointer<Group> groupMerged = dbDestination->rootGroup()->findChildByName("group2 renamed");
971
    QCOMPARE(groupMerged->notes(), QString("updated notes"));
972
    QCOMPARE(groupMerged->iconUuid(), customIconId);
973
}
974

975
void TestMerge::testUpdateGroupLocation()
976
{
977
    QScopedPointer<Database> dbDestination(createTestDatabase());
978
    auto group3DestinationCreated = new Group();
979
    QUuid group3Uuid = QUuid::createUuid();
980
    group3DestinationCreated->setUuid(group3Uuid);
981
    group3DestinationCreated->setName("group3");
982
    group3DestinationCreated->setParent(dbDestination->rootGroup()->findChildByName("group1"));
983

984
    QScopedPointer<Database> dbSource(
985
        createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
986

987
    // Sanity check
988
    QPointer<Group> group3SourceInitial = dbSource->rootGroup()->findGroupByUuid(group3Uuid);
989
    QVERIFY(group3DestinationCreated != nullptr);
990

991
    QDateTime initialLocationChanged = group3SourceInitial->timeInfo().locationChanged();
992

993
    m_clock->advanceSecond(1);
994

995
    QPointer<Group> group3SourceMoved = dbSource->rootGroup()->findGroupByUuid(group3Uuid);
996
    QVERIFY(group3SourceMoved != nullptr);
997
    group3SourceMoved->setParent(dbSource->rootGroup()->findChildByName("group2"));
998

999
    QDateTime movedLocaltionChanged = group3SourceMoved->timeInfo().locationChanged();
1000
    QVERIFY(initialLocationChanged < movedLocaltionChanged);
1001

1002
    m_clock->advanceSecond(1);
1003

1004
    Merger merger1(dbSource.data(), dbDestination.data());
1005
    merger1.merge();
1006

1007
    QPointer<Group> group3DestinationMerged1 = dbDestination->rootGroup()->findGroupByUuid(group3Uuid);
1008
    QVERIFY(group3DestinationMerged1 != nullptr);
1009
    QCOMPARE(group3DestinationMerged1->parent(), dbDestination->rootGroup()->findChildByName("group2"));
1010
    QCOMPARE(group3DestinationMerged1->timeInfo().locationChanged(), movedLocaltionChanged);
1011

1012
    m_clock->advanceSecond(1);
1013

1014
    Merger merger2(dbSource.data(), dbDestination.data());
1015
    merger2.merge();
1016

1017
    QPointer<Group> group3DestinationMerged2 = dbDestination->rootGroup()->findGroupByUuid(group3Uuid);
1018
    QVERIFY(group3DestinationMerged2 != nullptr);
1019
    QCOMPARE(group3DestinationMerged2->parent(), dbDestination->rootGroup()->findChildByName("group2"));
1020
    QCOMPARE(group3DestinationMerged1->timeInfo().locationChanged(), movedLocaltionChanged);
1021
}
1022

1023
/**
1024
 * The first merge should create new entries, the
1025
 * second should only sync them, since they have
1026
 * been created with the same UUIDs.
1027
 */
1028
void TestMerge::testMergeAndSync()
1029
{
1030
    QScopedPointer<Database> dbDestination(new Database());
1031
    QScopedPointer<Database> dbSource(createTestDatabase());
1032

1033
    QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 0);
1034

1035
    m_clock->advanceSecond(1);
1036

1037
    Merger merger1(dbSource.data(), dbDestination.data());
1038
    merger1.merge();
1039

1040
    QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
1041

1042
    m_clock->advanceSecond(1);
1043

1044
    Merger merger2(dbSource.data(), dbDestination.data());
1045
    merger2.merge();
1046

1047
    // Still only 2 entries, since now we detect which are already present.
1048
    QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
1049
}
1050

1051
/**
1052
 * Custom icons should be brought over when merging.
1053
 */
1054
void TestMerge::testMergeCustomIcons()
1055
{
1056
    QScopedPointer<Database> dbDestination(new Database());
1057
    QScopedPointer<Database> dbSource(createTestDatabase());
1058

1059
    m_clock->advanceSecond(1);
1060

1061
    QUuid customIconId = QUuid::createUuid();
1062

1063
    dbSource->metadata()->addCustomIcon(customIconId, QString("custom icon").toLocal8Bit());
1064
    // Sanity check.
1065
    QVERIFY(dbSource->metadata()->hasCustomIcon(customIconId));
1066

1067
    m_clock->advanceSecond(1);
1068

1069
    Merger merger(dbSource.data(), dbDestination.data());
1070
    merger.merge();
1071

1072
    QVERIFY(dbDestination->metadata()->hasCustomIcon(customIconId));
1073
}
1074

1075
/**
1076
 * No duplicate icons should be created
1077
 */
1078
void TestMerge::testMergeDuplicateCustomIcons()
1079
{
1080
    QScopedPointer<Database> dbDestination(new Database());
1081
    QScopedPointer<Database> dbSource(createTestDatabase());
1082

1083
    m_clock->advanceSecond(1);
1084

1085
    QUuid customIconId = QUuid::createUuid();
1086

1087
    QByteArray customIcon1("custom icon 1");
1088
    QByteArray customIcon2("custom icon 2");
1089

1090
    dbSource->metadata()->addCustomIcon(customIconId, customIcon1);
1091
    dbDestination->metadata()->addCustomIcon(customIconId, customIcon2);
1092
    // Sanity check.
1093
    QVERIFY(dbSource->metadata()->hasCustomIcon(customIconId));
1094
    QVERIFY(dbDestination->metadata()->hasCustomIcon(customIconId));
1095

1096
    m_clock->advanceSecond(1);
1097

1098
    Merger merger(dbSource.data(), dbDestination.data());
1099
    merger.merge();
1100

1101
    QVERIFY(dbDestination->metadata()->hasCustomIcon(customIconId));
1102
    QCOMPARE(dbDestination->metadata()->customIconsOrder().count(), 1);
1103
    QCOMPARE(dbDestination->metadata()->customIcon(customIconId).data, customIcon2);
1104
}
1105

1106
void TestMerge::testMetadata()
1107
{
1108
    QSKIP("Sophisticated merging for Metadata not implemented");
1109
    // TODO HNH: I think a merge of recycle bins would be nice since duplicating them
1110
    //           is not really a good solution - the one to use as final recycle bin
1111
    //           is determined by the merge method - if only one has a bin, this one
1112
    //           will be used - exception is the target has no recycle bin activated
1113
}
1114

1115
void TestMerge::testCustomData()
1116
{
1117
    QScopedPointer<Database> dbDestination(new Database());
1118
    QScopedPointer<Database> dbSource(createTestDatabase());
1119
    QScopedPointer<Database> dbDestination2(new Database());
1120
    QScopedPointer<Database> dbSource2(createTestDatabase());
1121

1122
    m_clock->advanceSecond(1);
1123

1124
    dbDestination->metadata()->customData()->set("toBeDeleted", "value");
1125
    dbDestination->metadata()->customData()->set("key3", "oldValue");
1126

1127
    dbSource2->metadata()->customData()->set("key1", "value1");
1128
    dbSource2->metadata()->customData()->set("key2", "value2");
1129
    dbSource2->metadata()->customData()->set("key3", "newValue");
1130
    dbSource2->metadata()->customData()->set("Browser", "n'8=3W@L^6d->d.]St_>]");
1131

1132
    m_clock->advanceSecond(1);
1133

1134
    dbSource->metadata()->customData()->set("key1", "value1");
1135
    dbSource->metadata()->customData()->set("key2", "value2");
1136
    dbSource->metadata()->customData()->set("key3", "newValue");
1137
    dbSource->metadata()->customData()->set("Browser", "n'8=3W@L^6d->d.]St_>]");
1138

1139
    dbDestination2->metadata()->customData()->set("notToBeDeleted", "value");
1140
    dbDestination2->metadata()->customData()->set("key3", "oldValue");
1141

1142
    // Sanity check.
1143
    QVERIFY(!dbSource->metadata()->customData()->isEmpty());
1144
    QVERIFY(!dbSource2->metadata()->customData()->isEmpty());
1145

1146
    m_clock->advanceSecond(1);
1147

1148
    Merger merger(dbSource.data(), dbDestination.data());
1149
    QStringList changes = merger.merge();
1150

1151
    QVERIFY(!changes.isEmpty());
1152

1153
    // Source is newer, data should be merged
1154
    QVERIFY(!dbDestination->metadata()->customData()->isEmpty());
1155
    QVERIFY(dbDestination->metadata()->customData()->contains("key1"));
1156
    QVERIFY(dbDestination->metadata()->customData()->contains("key2"));
1157
    QVERIFY(dbDestination->metadata()->customData()->contains("Browser"));
1158
    QVERIFY(!dbDestination->metadata()->customData()->contains("toBeDeleted"));
1159
    QCOMPARE(dbDestination->metadata()->customData()->value("key1"), QString("value1"));
1160
    QCOMPARE(dbDestination->metadata()->customData()->value("key2"), QString("value2"));
1161
    QCOMPARE(dbDestination->metadata()->customData()->value("Browser"), QString("n'8=3W@L^6d->d.]St_>]"));
1162
    QCOMPARE(dbDestination->metadata()->customData()->value("key3"),
1163
             QString("newValue")); // Old value should be replaced
1164

1165
    // Merging again should not do anything if the values are the same.
1166
    m_clock->advanceSecond(1);
1167
    dbSource->metadata()->customData()->set("key3", "oldValue");
1168
    dbSource->metadata()->customData()->set("key3", "newValue");
1169
    Merger merger2(dbSource.data(), dbDestination.data());
1170
    QStringList changes2 = merger2.merge();
1171
    QVERIFY(changes2.isEmpty());
1172

1173
    Merger merger3(dbSource2.data(), dbDestination2.data());
1174
    merger3.merge();
1175

1176
    // Target is newer, no data is merged
1177
    QVERIFY(!dbDestination2->metadata()->customData()->isEmpty());
1178
    QVERIFY(!dbDestination2->metadata()->customData()->contains("key1"));
1179
    QVERIFY(!dbDestination2->metadata()->customData()->contains("key2"));
1180
    QVERIFY(!dbDestination2->metadata()->customData()->contains("Browser"));
1181
    QVERIFY(dbDestination2->metadata()->customData()->contains("notToBeDeleted"));
1182
    QCOMPARE(dbDestination2->metadata()->customData()->value("key3"),
1183
             QString("oldValue")); // Old value should not be replaced
1184
}
1185

1186
void TestMerge::testDeletedEntry()
1187
{
1188
    QScopedPointer<Database> dbDestination(createTestDatabase());
1189
    QScopedPointer<Database> dbSource(
1190
        createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
1191

1192
    m_clock->advanceSecond(1);
1193

1194
    QPointer<Entry> entry1SourceInitial = dbSource->rootGroup()->findEntryByPath("entry1");
1195
    QVERIFY(entry1SourceInitial != nullptr);
1196
    QUuid entry1Uuid = entry1SourceInitial->uuid();
1197
    delete entry1SourceInitial;
1198
    QVERIFY(dbSource->containsDeletedObject(entry1Uuid));
1199

1200
    m_clock->advanceSecond(1);
1201

1202
    QPointer<Entry> entry2DestinationInitial = dbDestination->rootGroup()->findEntryByPath("entry2");
1203
    QVERIFY(entry2DestinationInitial != nullptr);
1204
    QUuid entry2Uuid = entry2DestinationInitial->uuid();
1205
    delete entry2DestinationInitial;
1206
    QVERIFY(dbDestination->containsDeletedObject(entry2Uuid));
1207

1208
    m_clock->advanceSecond(1);
1209

1210
    Merger merger(dbSource.data(), dbDestination.data());
1211
    merger.merge();
1212

1213
    QPointer<Entry> entry1DestinationMerged = dbDestination->rootGroup()->findEntryByPath("entry1");
1214
    QVERIFY(entry1DestinationMerged);
1215
    QVERIFY(!dbDestination->containsDeletedObject(entry1Uuid));
1216
    QPointer<Entry> entry2DestinationMerged = dbDestination->rootGroup()->findEntryByPath("entry2");
1217
    QVERIFY(entry2DestinationMerged);
1218
    // Uuid in db and deletedObjects is intended according to KeePass #1752
1219
    QVERIFY(dbDestination->containsDeletedObject(entry2Uuid));
1220

1221
    QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2);
1222
}
1223

1224
void TestMerge::testDeletedGroup()
1225
{
1226
    QScopedPointer<Database> dbDestination(createTestDatabase());
1227
    QScopedPointer<Database> dbSource(
1228
        createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
1229

1230
    m_clock->advanceSecond(1);
1231

1232
    QPointer<Group> group2DestinationInitial = dbDestination->rootGroup()->findChildByName("group2");
1233
    QVERIFY(group2DestinationInitial != nullptr);
1234
    auto entry3DestinationCreated = new Entry();
1235
    entry3DestinationCreated->beginUpdate();
1236
    entry3DestinationCreated->setUuid(QUuid::createUuid());
1237
    entry3DestinationCreated->setGroup(group2DestinationInitial);
1238
    entry3DestinationCreated->setTitle("entry3");
1239
    entry3DestinationCreated->endUpdate();
1240

1241
    m_clock->advanceSecond(1);
1242

1243
    QPointer<Group> group1SourceInitial = dbSource->rootGroup()->findChildByName("group1");
1244
    QVERIFY(group1SourceInitial != nullptr);
1245
    QPointer<Entry> entry1SourceInitial = dbSource->rootGroup()->findEntryByPath("entry1");
1246
    QVERIFY(entry1SourceInitial != nullptr);
1247
    QPointer<Entry> entry2SourceInitial = dbSource->rootGroup()->findEntryByPath("entry2");
1248
    QVERIFY(entry2SourceInitial != nullptr);
1249
    QUuid group1Uuid = group1SourceInitial->uuid();
1250
    QUuid entry1Uuid = entry1SourceInitial->uuid();
1251
    QUuid entry2Uuid = entry2SourceInitial->uuid();
1252
    delete group1SourceInitial;
1253
    QVERIFY(dbSource->containsDeletedObject(group1Uuid));
1254
    QVERIFY(dbSource->containsDeletedObject(entry1Uuid));
1255
    QVERIFY(dbSource->containsDeletedObject(entry2Uuid));
1256

1257
    m_clock->advanceSecond(1);
1258

1259
    QPointer<Group> group2SourceInitial = dbSource->rootGroup()->findChildByName("group2");
1260
    QVERIFY(group2SourceInitial != nullptr);
1261
    QUuid group2Uuid = group2SourceInitial->uuid();
1262
    delete group2SourceInitial;
1263
    QVERIFY(dbSource->containsDeletedObject(group2Uuid));
1264

1265
    m_clock->advanceSecond(1);
1266

1267
    Merger merger(dbSource.data(), dbDestination.data());
1268
    merger.merge();
1269

1270
    QVERIFY(!dbDestination->containsDeletedObject(group1Uuid));
1271
    QVERIFY(!dbDestination->containsDeletedObject(entry1Uuid));
1272
    QVERIFY(!dbDestination->containsDeletedObject(entry2Uuid));
1273
    QVERIFY(!dbDestination->containsDeletedObject(group2Uuid));
1274

1275
    QPointer<Entry> entry1DestinationMerged = dbDestination->rootGroup()->findEntryByPath("entry1");
1276
    QVERIFY(entry1DestinationMerged);
1277
    QPointer<Entry> entry2DestinationMerged = dbDestination->rootGroup()->findEntryByPath("entry2");
1278
    QVERIFY(entry2DestinationMerged);
1279
    QPointer<Entry> entry3DestinationMerged = dbDestination->rootGroup()->findEntryByPath("entry3");
1280
    QVERIFY(entry3DestinationMerged);
1281
    QPointer<Group> group1DestinationMerged = dbDestination->rootGroup()->findChildByName("group1");
1282
    QVERIFY(group1DestinationMerged);
1283
    QPointer<Group> group2DestinationMerged = dbDestination->rootGroup()->findChildByName("group2");
1284
    QVERIFY(group2DestinationMerged);
1285

1286
    QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 3);
1287
}
1288

1289
void TestMerge::testDeletedRevertedEntry()
1290
{
1291
    QScopedPointer<Database> dbDestination(createTestDatabase());
1292
    QScopedPointer<Database> dbSource(
1293
        createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
1294

1295
    m_clock->advanceSecond(1);
1296

1297
    QPointer<Entry> entry1DestinationInitial = dbDestination->rootGroup()->findEntryByPath("entry1");
1298
    QVERIFY(entry1DestinationInitial != nullptr);
1299
    QUuid entry1Uuid = entry1DestinationInitial->uuid();
1300
    delete entry1DestinationInitial;
1301
    QVERIFY(dbDestination->containsDeletedObject(entry1Uuid));
1302

1303
    m_clock->advanceSecond(1);
1304

1305
    QPointer<Entry> entry2SourceInitial = dbSource->rootGroup()->findEntryByPath("entry2");
1306
    QVERIFY(entry2SourceInitial != nullptr);
1307
    QUuid entry2Uuid = entry2SourceInitial->uuid();
1308
    delete entry2SourceInitial;
1309
    QVERIFY(dbSource->containsDeletedObject(entry2Uuid));
1310

1311
    m_clock->advanceSecond(1);
1312

1313
    QPointer<Entry> entry1SourceInitial = dbSource->rootGroup()->findEntryByPath("entry1");
1314
    QVERIFY(entry1SourceInitial != nullptr);
1315
    entry1SourceInitial->setNotes("Updated");
1316

1317
    QPointer<Entry> entry2DestinationInitial = dbDestination->rootGroup()->findEntryByPath("entry2");
1318
    QVERIFY(entry2DestinationInitial != nullptr);
1319
    entry2DestinationInitial->setNotes("Updated");
1320

1321
    Merger merger(dbSource.data(), dbDestination.data());
1322
    merger.merge();
1323

1324
    // Uuid in db and deletedObjects is intended according to KeePass #1752
1325
    QVERIFY(dbDestination->containsDeletedObject(entry1Uuid));
1326
    QVERIFY(!dbDestination->containsDeletedObject(entry2Uuid));
1327

1328
    QPointer<Entry> entry1DestinationMerged = dbDestination->rootGroup()->findEntryByPath("entry1");
1329
    QVERIFY(entry1DestinationMerged);
1330
    QVERIFY(entry1DestinationMerged->notes() == "Updated");
1331
    QPointer<Entry> entry2DestinationMerged = dbDestination->rootGroup()->findEntryByPath("entry2");
1332
    QVERIFY(entry2DestinationMerged);
1333
    QVERIFY(entry2DestinationMerged->notes() == "Updated");
1334
}
1335

1336
void TestMerge::testDeletedRevertedGroup()
1337
{
1338
    QScopedPointer<Database> dbDestination(createTestDatabase());
1339
    QScopedPointer<Database> dbSource(
1340
        createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
1341

1342
    m_clock->advanceSecond(1);
1343

1344
    QPointer<Group> group2SourceInitial = dbSource->rootGroup()->findChildByName("group2");
1345
    QVERIFY(group2SourceInitial);
1346
    QUuid group2Uuid = group2SourceInitial->uuid();
1347
    delete group2SourceInitial;
1348
    QVERIFY(dbSource->containsDeletedObject(group2Uuid));
1349

1350
    m_clock->advanceSecond(1);
1351

1352
    QPointer<Group> group1DestinationInitial = dbDestination->rootGroup()->findChildByName("group1");
1353
    QVERIFY(group1DestinationInitial);
1354
    QUuid group1Uuid = group1DestinationInitial->uuid();
1355
    delete group1DestinationInitial;
1356
    QVERIFY(dbDestination->containsDeletedObject(group1Uuid));
1357

1358
    m_clock->advanceSecond(1);
1359

1360
    QPointer<Group> group1SourceInitial = dbSource->rootGroup()->findChildByName("group1");
1361
    QVERIFY(group1SourceInitial);
1362
    group1SourceInitial->setNotes("Updated");
1363

1364
    m_clock->advanceSecond(1);
1365

1366
    QPointer<Group> group2DestinationInitial = dbDestination->rootGroup()->findChildByName("group2");
1367
    QVERIFY(group2DestinationInitial);
1368
    group2DestinationInitial->setNotes("Updated");
1369

1370
    m_clock->advanceSecond(1);
1371

1372
    Merger merger(dbSource.data(), dbDestination.data());
1373
    merger.merge();
1374

1375
    // Uuid in db and deletedObjects is intended according to KeePass #1752
1376
    QVERIFY(dbDestination->containsDeletedObject(group1Uuid));
1377
    QVERIFY(!dbDestination->containsDeletedObject(group2Uuid));
1378

1379
    QPointer<Group> group1DestinationMerged = dbDestination->rootGroup()->findChildByName("group1");
1380
    QVERIFY(group1DestinationMerged);
1381
    QVERIFY(group1DestinationMerged->notes() == "Updated");
1382
    QPointer<Group> group2DestinationMerged = dbDestination->rootGroup()->findChildByName("group2");
1383
    QVERIFY(group2DestinationMerged);
1384
    QVERIFY(group2DestinationMerged->notes() == "Updated");
1385
}
1386

1387
/**
1388
 * If the group is updated in the source database, and the
1389
 * destination database after, the group should remain the
1390
 * same.
1391
 */
1392
void TestMerge::testResolveGroupConflictOlder()
1393
{
1394
    QScopedPointer<Database> dbDestination(createTestDatabase());
1395
    QScopedPointer<Database> dbSource(
1396
        createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
1397

1398
    // sanity check
1399
    QPointer<Group> groupSourceInitial = dbSource->rootGroup()->findChildByName("group1");
1400
    QVERIFY(groupSourceInitial != nullptr);
1401

1402
    // Make sure the two changes have a different timestamp.
1403
    m_clock->advanceSecond(1);
1404

1405
    groupSourceInitial->setName("group1 updated in source");
1406

1407
    // Make sure the two changes have a different timestamp.
1408
    m_clock->advanceSecond(1);
1409

1410
    QPointer<Group> groupDestinationUpdated = dbDestination->rootGroup()->findChildByName("group1");
1411
    groupDestinationUpdated->setName("group1 updated in destination");
1412

1413
    m_clock->advanceSecond(1);
1414

1415
    Merger merger(dbSource.data(), dbDestination.data());
1416
    merger.merge();
1417

1418
    // sanity check
1419
    QPointer<Group> groupDestinationMerged =
1420
        dbDestination->rootGroup()->findChildByName("group1 updated in destination");
1421
    QVERIFY(groupDestinationMerged != nullptr);
1422
}
1423

1424
void TestMerge::testMergeNotModified()
1425
{
1426
    QScopedPointer<Database> dbDestination(createTestDatabase());
1427
    QScopedPointer<Database> dbSource(
1428
        createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
1429

1430
    QSignalSpy modifiedSignalSpy(dbDestination.data(), SIGNAL(modified()));
1431
    Merger merger(dbSource.data(), dbDestination.data());
1432
    merger.merge();
1433
    QTRY_VERIFY(modifiedSignalSpy.empty());
1434
}
1435

1436
void TestMerge::testMergeModified()
1437
{
1438
    QScopedPointer<Database> dbDestination(createTestDatabase());
1439
    QScopedPointer<Database> dbSource(
1440
        createTestDatabaseStructureClone(dbDestination.data(), Entry::CloneNoFlags, Group::CloneIncludeEntries));
1441

1442
    QSignalSpy modifiedSignalSpy(dbDestination.data(), SIGNAL(modified()));
1443
    // Make sure the two changes have a different timestamp.
1444
    QTest::qSleep(1);
1445
    Entry* entry = dbSource->rootGroup()->findEntryByPath("entry1");
1446
    entry->beginUpdate();
1447
    entry->setTitle("new title");
1448
    entry->endUpdate();
1449

1450
    Merger merger(dbSource.data(), dbDestination.data());
1451
    merger.merge();
1452
    QTRY_VERIFY(!modifiedSignalSpy.empty());
1453
}
1454

1455
Database* TestMerge::createTestDatabase()
1456
{
1457
    auto db = new Database();
1458

1459
    auto group1 = new Group();
1460
    group1->setName("group1");
1461
    group1->setUuid(QUuid::createUuid());
1462

1463
    auto group2 = new Group();
1464
    group2->setName("group2");
1465
    group2->setUuid(QUuid::createUuid());
1466

1467
    auto entry1 = new Entry();
1468
    entry1->setUuid(QUuid::createUuid());
1469
    auto entry2 = new Entry();
1470
    entry2->setUuid(QUuid::createUuid());
1471

1472
    m_clock->advanceYear(1);
1473

1474
    // Give Entry 1 a history
1475
    entry1->beginUpdate();
1476
    entry1->setGroup(group1);
1477
    entry1->setTitle("entry1");
1478
    entry1->endUpdate();
1479

1480
    // Give Entry 2 a history
1481
    entry2->beginUpdate();
1482
    entry2->setGroup(group1);
1483
    entry2->setTitle("entry2");
1484
    entry2->endUpdate();
1485

1486
    group1->setParent(db->rootGroup());
1487
    group2->setParent(db->rootGroup());
1488

1489
    return db;
1490
}
1491

1492
Database* TestMerge::createTestDatabaseStructureClone(Database* source, int entryFlags, int groupFlags)
1493
{
1494
    auto db = new Database();
1495
    auto oldGroup = db->setRootGroup(source->rootGroup()->clone(static_cast<Entry::CloneFlag>(entryFlags),
1496
                                                                static_cast<Group::CloneFlag>(groupFlags)));
1497
    delete oldGroup;
1498
    return db;
1499
}
1500

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.