keepassxc

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

19
#include "TestGui.h"
20
#include "gui/Application.h"
21

22
#include <QCheckBox>
23
#include <QClipboard>
24
#include <QMimeData>
25
#include <QPlainTextEdit>
26
#include <QPushButton>
27
#include <QRadioButton>
28
#include <QSignalSpy>
29
#include <QSpinBox>
30
#include <QTest>
31
#include <QToolBar>
32

33
#include "config-keepassx-tests.h"
34
#include "core/PasswordHealth.h"
35
#include "core/Tools.h"
36
#include "crypto/Crypto.h"
37
#include "gui/ActionCollection.h"
38
#include "gui/ApplicationSettingsWidget.h"
39
#include "gui/CategoryListWidget.h"
40
#include "gui/CloneDialog.h"
41
#include "gui/DatabaseTabWidget.h"
42
#include "gui/EntryPreviewWidget.h"
43
#include "gui/FileDialog.h"
44
#include "gui/MessageBox.h"
45
#include "gui/PasswordGeneratorWidget.h"
46
#include "gui/PasswordWidget.h"
47
#include "gui/SearchWidget.h"
48
#include "gui/ShortcutSettingsPage.h"
49
#include "gui/TotpDialog.h"
50
#include "gui/TotpSetupDialog.h"
51
#include "gui/databasekey/KeyFileEditWidget.h"
52
#include "gui/databasekey/PasswordEditWidget.h"
53
#include "gui/dbsettings/DatabaseSettingsDialog.h"
54
#include "gui/entry/EditEntryWidget.h"
55
#include "gui/entry/EntryView.h"
56
#include "gui/group/EditGroupWidget.h"
57
#include "gui/group/GroupModel.h"
58
#include "gui/group/GroupView.h"
59
#include "gui/tag/TagsEdit.h"
60
#include "gui/wizard/NewDatabaseWizard.h"
61
#include "keys/FileKey.h"
62

63
#define TEST_MODAL_NO_WAIT(TEST_CODE)                                                                                  \
64
    bool dialogFinished = false;                                                                                       \
65
    QTimer::singleShot(0, [&]() { TEST_CODE dialogFinished = true; })
66

67
#define TEST_MODAL(TEST_CODE)                                                                                          \
68
    TEST_MODAL_NO_WAIT(TEST_CODE);                                                                                     \
69
    QTRY_VERIFY(dialogFinished)
70

71
int main(int argc, char* argv[])
72
{
73
#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
74
    QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
75
    QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
76
#endif
77
    Application app(argc, argv);
78
    app.setApplicationName("KeePassXC");
79
    app.setApplicationVersion(KEEPASSXC_VERSION);
80
    app.setQuitOnLastWindowClosed(false);
81
    app.setAttribute(Qt::AA_Use96Dpi, true);
82
    app.applyTheme();
83
    QTEST_DISABLE_KEYPAD_NAVIGATION
84
    TestGui tc;
85
    QTEST_SET_MAIN_SOURCE_PATH
86
    return QTest::qExec(&tc, argc, argv);
87
}
88

89
void TestGui::initTestCase()
90
{
91
    QVERIFY(Crypto::init());
92
    Config::createTempFileInstance();
93
    QLocale::setDefault(QLocale::c());
94
    Application::bootstrap();
95

96
    m_mainWindow.reset(new MainWindow());
97
    m_tabWidget = m_mainWindow->findChild<DatabaseTabWidget*>("tabWidget");
98
    m_statusBarLabel = m_mainWindow->findChild<QLabel*>("statusBarLabel");
99
    m_mainWindow->show();
100
    m_mainWindow->resize(1024, 768);
101
}
102

103
// Every test starts with resetting config settings and opening the temp database
104
void TestGui::init()
105
{
106
    // Reset config to defaults
107
    config()->resetToDefaults();
108
    // Disable autosave so we can test the modified file indicator
109
    config()->set(Config::AutoSaveAfterEveryChange, false);
110
    config()->set(Config::AutoSaveOnExit, false);
111
    // Enable the tray icon so we can test hiding/restoring the windowQByteArray
112
    config()->set(Config::GUI_ShowTrayIcon, true);
113
    // Disable the update check first time alert
114
    config()->set(Config::UpdateCheckMessageShown, true);
115
    // Disable quick unlock
116
    config()->set(Config::Security_QuickUnlock, false);
117
    // Disable atomic saves to prevent transient errors on some platforms
118
    config()->set(Config::UseAtomicSaves, false);
119
    // Disable showing expired entries on unlock
120
    config()->set(Config::GUI_ShowExpiredEntriesOnDatabaseUnlock, false);
121

122
    // Copy the test database file to the temporary file
123
    auto origFilePath = QDir(KEEPASSX_TEST_DATA_DIR).absoluteFilePath("NewDatabase.kdbx");
124
    QVERIFY(m_dbFile.copyFromFile(origFilePath));
125

126
    m_dbFileName = QFileInfo(m_dbFile.fileName()).fileName();
127
    m_dbFilePath = m_dbFile.fileName();
128

129
    // make sure window is activated or focus tests may fail
130
    m_mainWindow->activateWindow();
131
    QApplication::processEvents();
132

133
    fileDialog()->setNextFileName(m_dbFilePath);
134
    triggerAction("actionDatabaseOpen");
135

136
    QApplication::processEvents();
137

138
    m_dbWidget = m_tabWidget->currentDatabaseWidget();
139
    auto* databaseOpenWidget = m_tabWidget->currentDatabaseWidget()->findChild<QWidget*>("databaseOpenWidget");
140
    QVERIFY(databaseOpenWidget);
141
    // editPassword is not QLineEdit anymore but PasswordWidget
142
    auto* editPassword =
143
        databaseOpenWidget->findChild<PasswordWidget*>("editPassword")->findChild<QLineEdit*>("passwordEdit");
144
    QVERIFY(editPassword);
145
    editPassword->setFocus();
146
    QTRY_VERIFY(editPassword->hasFocus());
147

148
    QTest::keyClicks(editPassword, "a");
149
    QTest::keyClick(editPassword, Qt::Key_Enter);
150

151
    QTRY_VERIFY(!m_dbWidget->isLocked());
152
    m_db = m_dbWidget->database();
153

154
    QApplication::processEvents();
155
}
156

157
// Every test ends with closing the temp database without saving
158
void TestGui::cleanup()
159
{
160
    // DO NOT save the database
161
    m_db->markAsClean();
162
    MessageBox::setNextAnswer(MessageBox::No);
163
    triggerAction("actionDatabaseClose");
164
    QApplication::processEvents();
165
    MessageBox::setNextAnswer(MessageBox::NoButton);
166

167
    if (m_dbWidget) {
168
        delete m_dbWidget;
169
    }
170
}
171

172
void TestGui::cleanupTestCase()
173
{
174
    m_dbFile.remove();
175
}
176

177
void TestGui::testSettingsDefaultTabOrder()
178
{
179
    // check application settings default tab order
180
    triggerAction("actionSettings");
181
    auto* settingsWidget = m_mainWindow->findChild<ApplicationSettingsWidget*>();
182
    QVERIFY(settingsWidget->isVisible());
183
    QCOMPARE(settingsWidget->findChild<CategoryListWidget*>("categoryList")->currentCategory(), 0);
184
    for (auto* w : settingsWidget->findChildren<QTabWidget*>()) {
185
        if (w->currentIndex() != 0) {
186
            QFAIL("Application settings contain QTabWidgets whose default index is not 0");
187
        }
188
    }
189
    QTest::keyClick(settingsWidget, Qt::Key::Key_Escape);
190

191
    // check database settings default tab order
192
    triggerAction("actionDatabaseSettings");
193
    auto* dbSettingsWidget = m_mainWindow->findChild<DatabaseSettingsDialog*>();
194
    QVERIFY(dbSettingsWidget->isVisible());
195
    QCOMPARE(dbSettingsWidget->findChild<CategoryListWidget*>("categoryList")->currentCategory(), 0);
196
    for (auto* w : dbSettingsWidget->findChildren<QTabWidget*>()) {
197
        if (w->currentIndex() != 0 && w->objectName() != "encryptionSettingsTabWidget") {
198
            QFAIL("Database settings contain QTabWidgets whose default index is not 0");
199
        }
200
    }
201
    QTest::keyClick(dbSettingsWidget, Qt::Key::Key_Escape);
202
}
203

204
void TestGui::testCreateDatabase()
205
{
206
    TEST_MODAL_NO_WAIT(
207
        NewDatabaseWizard * wizard; QTRY_VERIFY(wizard = m_tabWidget->findChild<NewDatabaseWizard*>());
208

209
        QTest::keyClicks(wizard->currentPage()->findChild<QLineEdit*>("databaseName"), "Test Name");
210
        QTest::keyClicks(wizard->currentPage()->findChild<QLineEdit*>("databaseDescription"), "Test Description");
211
        QCOMPARE(wizard->currentId(), 0);
212

213
        QTest::keyClick(wizard, Qt::Key_Enter);
214
        QCOMPARE(wizard->currentId(), 1);
215

216
        // Check that basic encryption settings are visible
217
        auto decryptionTimeSlider = wizard->currentPage()->findChild<QSlider*>("decryptionTimeSlider");
218
        auto algorithmComboBox = wizard->currentPage()->findChild<QComboBox*>("algorithmComboBox");
219
        QTRY_VERIFY(decryptionTimeSlider->isVisible());
220
        QVERIFY(!algorithmComboBox->isVisible());
221

222
        // Set the encryption settings to the advanced view
223
        auto encryptionSettings = wizard->currentPage()->findChild<QTabWidget*>("encryptionSettingsTabWidget");
224
        auto advancedTab = encryptionSettings->findChild<QWidget*>("advancedTab");
225
        encryptionSettings->setCurrentWidget(advancedTab);
226
        QTRY_VERIFY(!decryptionTimeSlider->isVisible());
227
        QVERIFY(algorithmComboBox->isVisible());
228

229
        auto rounds = wizard->currentPage()->findChild<QSpinBox*>("transformRoundsSpinBox");
230
        QVERIFY(rounds);
231
        QVERIFY(rounds->isVisible());
232
        QTest::mouseClick(rounds, Qt::MouseButton::LeftButton);
233
        QTest::keyClick(rounds, Qt::Key_A, Qt::ControlModifier);
234
        QTest::keyClicks(rounds, "2");
235
        QTest::keyClick(rounds, Qt::Key_Tab);
236
        QTest::keyClick(rounds, Qt::Key_Tab);
237

238
        auto memory = wizard->currentPage()->findChild<QSpinBox*>("memorySpinBox");
239
        QVERIFY(memory);
240
        QVERIFY(memory->isVisible());
241
        QTest::mouseClick(memory, Qt::MouseButton::LeftButton);
242
        QTest::keyClick(memory, Qt::Key_A, Qt::ControlModifier);
243
        QTest::keyClicks(memory, "50");
244
        QTest::keyClick(memory, Qt::Key_Tab);
245

246
        auto parallelism = wizard->currentPage()->findChild<QSpinBox*>("parallelismSpinBox");
247
        QVERIFY(parallelism);
248
        QVERIFY(parallelism->isVisible());
249
        QTest::mouseClick(parallelism, Qt::MouseButton::LeftButton);
250
        QTest::keyClick(parallelism, Qt::Key_A, Qt::ControlModifier);
251
        QTest::keyClicks(parallelism, "1");
252
        QTest::keyClick(parallelism, Qt::Key_Enter);
253

254
        QCOMPARE(wizard->currentId(), 2);
255

256
        // enter password
257
        auto* passwordWidget = wizard->currentPage()->findChild<PasswordEditWidget*>();
258
        QCOMPARE(passwordWidget->visiblePage(), KeyFileEditWidget::Page::Edit);
259
        auto* passwordEdit =
260
            passwordWidget->findChild<PasswordWidget*>("enterPasswordEdit")->findChild<QLineEdit*>("passwordEdit");
261
        auto* passwordRepeatEdit =
262
            passwordWidget->findChild<PasswordWidget*>("repeatPasswordEdit")->findChild<QLineEdit*>("passwordEdit");
263
        QTRY_VERIFY(passwordEdit->isVisible());
264
        QTRY_VERIFY(passwordEdit->hasFocus());
265
        QTest::keyClicks(passwordEdit, "test");
266
        QTest::keyClick(passwordEdit, Qt::Key::Key_Tab);
267
        QTest::keyClicks(passwordRepeatEdit, "test");
268

269
        // add key file
270
        auto* additionalOptionsButton = wizard->currentPage()->findChild<QPushButton*>("additionalKeyOptionsToggle");
271
        auto* keyFileWidget = wizard->currentPage()->findChild<KeyFileEditWidget*>();
272
        QVERIFY(additionalOptionsButton->isVisible());
273
        QTest::mouseClick(additionalOptionsButton, Qt::MouseButton::LeftButton);
274
        QTRY_VERIFY(keyFileWidget->isVisible());
275
        QTRY_VERIFY(!additionalOptionsButton->isVisible());
276
        QCOMPARE(passwordWidget->visiblePage(), KeyFileEditWidget::Page::Edit);
277
        QTest::mouseClick(keyFileWidget->findChild<QPushButton*>("addButton"), Qt::MouseButton::LeftButton);
278
        auto* fileEdit = keyFileWidget->findChild<QLineEdit*>("keyFileLineEdit");
279
        QTRY_VERIFY(fileEdit);
280
        QTRY_VERIFY(fileEdit->isVisible());
281
        fileDialog()->setNextFileName(QString("%1/%2").arg(QString(KEEPASSX_TEST_DATA_DIR), "FileKeyHashed.key"));
282
        QTest::keyClick(keyFileWidget->findChild<QPushButton*>("addButton"), Qt::Key::Key_Enter);
283
        QVERIFY(fileEdit->hasFocus());
284
        auto* browseButton = keyFileWidget->findChild<QPushButton*>("browseKeyFileButton");
285
        QTest::keyClick(browseButton, Qt::Key::Key_Enter);
286
        QCOMPARE(fileEdit->text(), QString("%1/%2").arg(QString(KEEPASSX_TEST_DATA_DIR), "FileKeyHashed.key"));
287

288
        // save database to temporary file
289
        TemporaryFile tmpFile;
290
        QVERIFY(tmpFile.open());
291
        tmpFile.close();
292
        fileDialog()->setNextFileName(tmpFile.fileName());
293

294
        // click Continue on the warning due to weak password
295
        MessageBox::setNextAnswer(MessageBox::ContinueWithWeakPass);
296
        QTest::keyClick(fileEdit, Qt::Key::Key_Enter);
297

298
        tmpFile.remove(););
299

300
    triggerAction("actionDatabaseNew");
301

302
    QCOMPARE(m_tabWidget->count(), 2);
303

304
    checkStatusBarText("0 Ent");
305

306
    // there is a new empty db
307
    m_db = m_tabWidget->currentDatabaseWidget()->database();
308
    QCOMPARE(m_db->rootGroup()->children().size(), 0);
309

310
    // check meta data
311
    QCOMPARE(m_db->metadata()->name(), QString("Test Name"));
312
    QCOMPARE(m_db->metadata()->description(), QString("Test Description"));
313

314
    // check key and encryption
315
    QCOMPARE(m_db->key()->keys().size(), 2);
316
    QCOMPARE(m_db->kdf()->rounds(), 2);
317
    QCOMPARE(m_db->kdf()->uuid(), KeePass2::KDF_ARGON2D);
318
    QCOMPARE(m_db->cipher(), KeePass2::CIPHER_AES256);
319
    auto compositeKey = QSharedPointer<CompositeKey>::create();
320
    compositeKey->addKey(QSharedPointer<PasswordKey>::create("test"));
321
    auto fileKey = QSharedPointer<FileKey>::create();
322
    fileKey->load(QString("%1/%2").arg(QString(KEEPASSX_TEST_DATA_DIR), "FileKeyHashed.key"));
323
    compositeKey->addKey(fileKey);
324
    QCOMPARE(m_db->key()->rawKey(), compositeKey->rawKey());
325

326
    checkStatusBarText("0 Ent");
327

328
    // Test the switching to other DB tab
329
    m_tabWidget->setCurrentIndex(0);
330
    checkStatusBarText("1 Ent");
331

332
    m_tabWidget->setCurrentIndex(1);
333
    checkStatusBarText("0 Ent");
334

335
    // close the new database
336
    MessageBox::setNextAnswer(MessageBox::No);
337
    triggerAction("actionDatabaseClose");
338

339
    // Wait for dialog to terminate
340
    QTRY_VERIFY(dialogFinished);
341
}
342

343
void TestGui::testMergeDatabase()
344
{
345
    // It is safe to ignore the warning this line produces
346
    QSignalSpy dbMergeSpy(m_dbWidget.data(), SIGNAL(databaseMerged(QSharedPointer<Database>)));
347
    QApplication::processEvents();
348

349
    // set file to merge from
350
    fileDialog()->setNextFileName(QString(KEEPASSX_TEST_DATA_DIR).append("/MergeDatabase.kdbx"));
351
    triggerAction("actionDatabaseMerge");
352

353
    QTRY_COMPARE(QApplication::focusWidget()->objectName(), QString("passwordEdit"));
354
    auto* editPasswordMerge = QApplication::focusWidget();
355
    QVERIFY(editPasswordMerge->isVisible());
356

357
    QTest::keyClicks(editPasswordMerge, "a");
358
    QTest::keyClick(editPasswordMerge, Qt::Key_Enter);
359

360
    QTRY_COMPARE(dbMergeSpy.count(), 1);
361
    QTRY_VERIFY(m_tabWidget->tabText(m_tabWidget->currentIndex()).contains("*"));
362

363
    m_db = m_tabWidget->currentDatabaseWidget()->database();
364

365
    // there are seven child groups of the root group
366
    QCOMPARE(m_db->rootGroup()->children().size(), 7);
367
    // the merged group should contain an entry
368
    QCOMPARE(m_db->rootGroup()->children().at(6)->entries().size(), 1);
369
    // the General group contains one entry merged from the other db
370
    QCOMPARE(m_db->rootGroup()->findChildByName("General")->entries().size(), 1);
371
}
372

373
void TestGui::testAutoreloadDatabase()
374
{
375
    config()->set(Config::AutoReloadOnChange, false);
376

377
    // Test accepting new file in autoreload
378
    MessageBox::setNextAnswer(MessageBox::Yes);
379
    // Overwrite the current database with the temp data
380
    QVERIFY(m_dbFile.copyFromFile(QString(KEEPASSX_TEST_DATA_DIR).append("/MergeDatabase.kdbx")));
381

382
    QTRY_VERIFY(m_db != m_dbWidget->database());
383
    m_db = m_dbWidget->database();
384

385
    // the General group contains one entry from the new db data
386
    QCOMPARE(m_db->rootGroup()->findChildByName("General")->entries().size(), 1);
387
    QVERIFY(!m_tabWidget->tabText(m_tabWidget->currentIndex()).endsWith("*"));
388

389
    // Reset the state
390
    cleanup();
391
    init();
392

393
    config()->set(Config::AutoReloadOnChange, false);
394

395
    // Test rejecting new file in autoreload
396
    MessageBox::setNextAnswer(MessageBox::No);
397
    // Overwrite the current database with the temp data
398
    QVERIFY(m_dbFile.copyFromFile(QString(KEEPASSX_TEST_DATA_DIR).append("/MergeDatabase.kdbx")));
399

400
    // Ensure the merge did not take place
401
    QCOMPARE(m_db->rootGroup()->findChildByName("General")->entries().size(), 0);
402
    QTRY_VERIFY(m_tabWidget->tabText(m_tabWidget->currentIndex()).endsWith("*"));
403

404
    // Reset the state
405
    cleanup();
406
    init();
407

408
    // Test accepting a merge of edits into autoreload
409
    // Turn on autoload so we only get one messagebox (for the merge)
410
    config()->set(Config::AutoReloadOnChange, true);
411
    // Modify some entries
412
    testEditEntry();
413

414
    // This is saying yes to merging the entries
415
    MessageBox::setNextAnswer(MessageBox::Merge);
416
    // Overwrite the current database with the temp data
417
    QVERIFY(m_dbFile.copyFromFile(QString(KEEPASSX_TEST_DATA_DIR).append("/MergeDatabase.kdbx")));
418

419
    QTRY_VERIFY(m_db != m_dbWidget->database());
420
    m_db = m_dbWidget->database();
421

422
    QCOMPARE(m_db->rootGroup()->findChildByName("General")->entries().size(), 1);
423
    QTRY_VERIFY(m_tabWidget->tabText(m_tabWidget->currentIndex()).endsWith("*"));
424
}
425

426
void TestGui::testTabs()
427
{
428
    QCOMPARE(m_tabWidget->count(), 1);
429
    QCOMPARE(m_tabWidget->tabText(m_tabWidget->currentIndex()), m_dbFileName);
430
}
431

432
void TestGui::testEditEntry()
433
{
434
    auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
435
    auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
436

437
    entryView->setFocus();
438
    QVERIFY(entryView->hasFocus());
439

440
    // Select the first entry in the database
441
    QModelIndex entryItem = entryView->model()->index(0, 1);
442
    Entry* entry = entryView->entryFromIndex(entryItem);
443
    clickIndex(entryItem, entryView, Qt::LeftButton);
444

445
    // Confirm the edit action button is enabled
446
    auto* entryEditAction = m_mainWindow->findChild<QAction*>("actionEntryEdit");
447
    QVERIFY(entryEditAction->isEnabled());
448
    QWidget* entryEditWidget = toolBar->widgetForAction(entryEditAction);
449
    QVERIFY(entryEditWidget->isVisible());
450
    QVERIFY(entryEditWidget->isEnabled());
451

452
    // Record current history count
453
    int editCount = entry->historyItems().size();
454

455
    // Edit the first entry ("Sample Entry")
456
    QTest::mouseClick(entryEditWidget, Qt::LeftButton);
457
    QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
458
    auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
459
    auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
460
    QTest::keyClicks(titleEdit, "_test");
461

462
    auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
463
    QVERIFY(editEntryWidgetButtonBox);
464
    auto* okButton = editEntryWidgetButtonBox->button(QDialogButtonBox::Ok);
465
    QVERIFY(okButton);
466
    auto* applyButton = editEntryWidgetButtonBox->button(QDialogButtonBox::Apply);
467
    QVERIFY(applyButton);
468

469
    // Apply the edit
470
    QTRY_VERIFY(applyButton->isEnabled());
471
    QTest::mouseClick(applyButton, Qt::LeftButton);
472
    QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
473
    QCOMPARE(entry->title(), QString("Sample Entry_test"));
474
    QCOMPARE(entry->historyItems().size(), ++editCount);
475
    QVERIFY(!applyButton->isEnabled());
476

477
    // Test the "known bad" checkbox
478
    editEntryWidget->setCurrentPage(1);
479
    auto excludeReportsCheckBox = editEntryWidget->findChild<QCheckBox*>("excludeReportsCheckBox");
480
    QVERIFY(excludeReportsCheckBox);
481
    QCOMPARE(excludeReportsCheckBox->isChecked(), false);
482
    excludeReportsCheckBox->setChecked(true);
483
    QTest::mouseClick(applyButton, Qt::LeftButton);
484
    QCOMPARE(entry->historyItems().size(), ++editCount);
485
    QVERIFY(entry->excludeFromReports());
486

487
    // Test tags
488
    auto* tags = editEntryWidget->findChild<TagsEdit*>("tagsList");
489
    QTest::keyClicks(tags, "_tag1");
490
    QTest::keyClick(tags, Qt::Key_Return);
491
    QCOMPARE(tags->tags().last(), QString("_tag1"));
492
    QTest::keyClicks(tags, "tag 2"); // adds another tag
493
    QTest::keyClick(tags, Qt::Key_Return);
494
    QCOMPARE(tags->tags().last(), QString("tag 2"));
495
    QTest::keyClick(tags, Qt::Key_Backspace); // Back into editing last tag
496
    QTest::keyClicks(tags, "_is!awesome");
497
    QTest::keyClick(tags, Qt::Key_Return);
498
    QCOMPARE(tags->tags().last(), QString("tag 2_is!awesome"));
499

500
    // Test entry colors (simulate choosing a color)
501
    editEntryWidget->setCurrentPage(1);
502
    auto fgColor = QString("#FF0000");
503
    auto bgColor = QString("#0000FF");
504
    // Set foreground color
505
    auto colorButton = editEntryWidget->findChild<QPushButton*>("fgColorButton");
506
    auto colorCheckBox = editEntryWidget->findChild<QCheckBox*>("fgColorCheckBox");
507
    colorButton->setProperty("color", fgColor);
508
    colorCheckBox->setChecked(true);
509
    // Set background color
510
    colorButton = editEntryWidget->findChild<QPushButton*>("bgColorButton");
511
    colorCheckBox = editEntryWidget->findChild<QCheckBox*>("bgColorCheckBox");
512
    colorButton->setProperty("color", bgColor);
513
    colorCheckBox->setChecked(true);
514
    QTest::mouseClick(applyButton, Qt::LeftButton);
515
    QCOMPARE(entry->historyItems().size(), ++editCount);
516

517
    // Test protected attributes
518
    editEntryWidget->setCurrentPage(1);
519
    auto* attrTextEdit = editEntryWidget->findChild<QPlainTextEdit*>("attributesEdit");
520
    QTest::mouseClick(editEntryWidget->findChild<QAbstractButton*>("addAttributeButton"), Qt::LeftButton);
521
    QString attrText = "TEST TEXT";
522
    QTest::keyClicks(attrTextEdit, attrText);
523
    QCOMPARE(attrTextEdit->toPlainText(), attrText);
524
    QTest::mouseClick(editEntryWidget->findChild<QAbstractButton*>("protectAttributeButton"), Qt::LeftButton);
525
    QVERIFY(attrTextEdit->toPlainText().contains("PROTECTED"));
526
    QTest::mouseClick(editEntryWidget->findChild<QAbstractButton*>("revealAttributeButton"), Qt::LeftButton);
527
    QCOMPARE(attrTextEdit->toPlainText(), attrText);
528
    editEntryWidget->setCurrentPage(0);
529

530
    // Save the edit (press OK)
531
    QTest::mouseClick(okButton, Qt::LeftButton);
532
    QApplication::processEvents();
533

534
    // Confirm edit was made
535
    QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
536
    QCOMPARE(entry->title(), QString("Sample Entry_test"));
537
    QCOMPARE(entry->foregroundColor().toUpper(), fgColor.toUpper());
538
    QCOMPARE(entryItem.data(Qt::ForegroundRole), QVariant(fgColor));
539
    QCOMPARE(entry->backgroundColor().toUpper(), bgColor.toUpper());
540
    QCOMPARE(entryItem.data(Qt::BackgroundRole), QVariant(bgColor));
541
    QCOMPARE(entry->historyItems().size(), ++editCount);
542

543
    // Confirm modified indicator is showing
544
    QTRY_COMPARE(m_tabWidget->tabText(m_tabWidget->currentIndex()), QString("%1*").arg(m_dbFileName));
545

546
    // Test copy & paste newline sanitization
547
    QTest::mouseClick(entryEditWidget, Qt::LeftButton);
548
    okButton = editEntryWidgetButtonBox->button(QDialogButtonBox::Ok);
549
    QVERIFY(okButton);
550
    QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
551
    titleEdit->setText("multiline\ntitle");
552
    editEntryWidget->findChild<QComboBox*>("usernameComboBox")->lineEdit()->setText("multiline\nusername");
553
    editEntryWidget->findChild<PasswordWidget*>("passwordEdit")->setText("multiline\npassword");
554
    editEntryWidget->findChild<QLineEdit*>("urlEdit")->setText("multiline\nurl");
555
    QTest::mouseClick(okButton, Qt::LeftButton);
556

557
    QCOMPARE(entry->title(), QString("multiline title"));
558
    QCOMPARE(entry->username(), QString("multiline username"));
559
    // here we keep newlines, so users can't lock themselves out accidentally
560
    QCOMPARE(entry->password(), QString("multiline\npassword"));
561
    QCOMPARE(entry->url(), QString("multiline url"));
562
}
563

564
void TestGui::testSearchEditEntry()
565
{
566
    // Regression test for Issue #1447 -- Uses example from issue description
567

568
    // Find buttons for group creation
569
    auto* editGroupWidget = m_dbWidget->findChild<EditGroupWidget*>("editGroupWidget");
570
    auto* nameEdit = editGroupWidget->findChild<QLineEdit*>("editName");
571
    auto* editGroupWidgetButtonBox = editGroupWidget->findChild<QDialogButtonBox*>("buttonBox");
572

573
    // Add groups "Good" and "Bad"
574
    m_dbWidget->createGroup();
575
    QTest::keyClicks(nameEdit, "Good");
576
    QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
577
    m_dbWidget->groupView()->setCurrentGroup(m_db->rootGroup()); // Makes "Good" and "Bad" on the same level
578
    m_dbWidget->createGroup();
579
    QTest::keyClicks(nameEdit, "Bad");
580
    QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
581
    m_dbWidget->groupView()->setCurrentGroup(m_db->rootGroup());
582

583
    // Find buttons for entry creation
584
    auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
585
    QWidget* entryNewWidget = toolBar->widgetForAction(m_mainWindow->findChild<QAction*>("actionEntryNew"));
586
    auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
587
    auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
588
    auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
589

590
    // Create "Doggy" in "Good"
591
    Group* goodGroup = m_dbWidget->currentGroup()->findChildByName(QString("Good"));
592
    m_dbWidget->groupView()->setCurrentGroup(goodGroup);
593
    QTest::mouseClick(entryNewWidget, Qt::LeftButton);
594
    QTest::keyClicks(titleEdit, "Doggy");
595
    QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
596
    // Select "Bad" group in groupView
597
    Group* badGroup = m_db->rootGroup()->findChildByName(QString("Bad"));
598
    m_dbWidget->groupView()->setCurrentGroup(badGroup);
599

600
    // Search for "Doggy" entry
601
    auto* searchWidget = toolBar->findChild<SearchWidget*>("SearchWidget");
602
    auto* searchTextEdit = searchWidget->findChild<QLineEdit*>("searchEdit");
603
    QTest::mouseClick(searchTextEdit, Qt::LeftButton);
604
    QTest::keyClicks(searchTextEdit, "Doggy");
605
    QTRY_VERIFY(m_dbWidget->isSearchActive());
606

607
    // Goto "Doggy"'s edit view
608
    QTest::keyClick(searchTextEdit, Qt::Key_Return);
609
    QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
610

611
    // Check the path in header is "parent-group > entry"
612
    QCOMPARE(m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget")->findChild<QLabel*>("headerLabel")->text(),
613
             QStringLiteral("Good \u2022 Doggy \u2022 Edit entry"));
614
}
615

616
void TestGui::testAddEntry()
617
{
618
    auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
619
    auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
620

621
    // Given the status bar label with initial number of entries.
622
    checkStatusBarText("1 Ent");
623

624
    // Find the new entry action
625
    auto* entryNewAction = m_mainWindow->findChild<QAction*>("actionEntryNew");
626
    QVERIFY(entryNewAction->isEnabled());
627

628
    // Find the button associated with the new entry action
629
    QWidget* entryNewWidget = toolBar->widgetForAction(entryNewAction);
630
    QVERIFY(entryNewWidget->isVisible());
631
    QVERIFY(entryNewWidget->isEnabled());
632

633
    // Click the new entry button and check that we enter edit mode
634
    QTest::mouseClick(entryNewWidget, Qt::LeftButton);
635
    QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
636

637
    // Add entry "test" and confirm added
638
    auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
639
    auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
640
    QTest::keyClicks(titleEdit, "test");
641
    auto* usernameComboBox = editEntryWidget->findChild<QComboBox*>("usernameComboBox");
642
    QVERIFY(usernameComboBox);
643
    QTest::mouseClick(usernameComboBox, Qt::LeftButton);
644
    QTest::keyClicks(usernameComboBox, "AutocompletionUsername");
645
    auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
646
    QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
647

648
    QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
649
    QModelIndex item = entryView->model()->index(1, 1);
650
    Entry* entry = entryView->entryFromIndex(item);
651

652
    QCOMPARE(entry->title(), QString("test"));
653
    QCOMPARE(entry->username(), QString("AutocompletionUsername"));
654
    QCOMPARE(entry->historyItems().size(), 0);
655

656
    m_db->updateCommonUsernames();
657

658
    // Then the status bar label should be updated with incremented number of entries.
659
    checkStatusBarText("2 Ent");
660

661
    // Add entry "something 2"
662
    QTest::mouseClick(entryNewWidget, Qt::LeftButton);
663
    QTest::keyClicks(titleEdit, "something 2");
664
    QTest::mouseClick(usernameComboBox, Qt::LeftButton);
665
    QTest::keyClicks(usernameComboBox, "Auto");
666
    QTest::keyPress(usernameComboBox, Qt::Key_Right);
667
    auto* passwordEdit =
668
        editEntryWidget->findChild<PasswordWidget*>("passwordEdit")->findChild<QLineEdit*>("passwordEdit");
669
    QTest::keyClicks(passwordEdit, "something 2");
670
    QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
671

672
    QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
673
    item = entryView->model()->index(1, 1);
674
    entry = entryView->entryFromIndex(item);
675

676
    QCOMPARE(entry->title(), QString("something 2"));
677
    QCOMPARE(entry->username(), QString("AutocompletionUsername"));
678
    QCOMPARE(entry->historyItems().size(), 0);
679

680
    // Add entry "something 5" but click cancel button (does NOT add entry)
681
    QTest::mouseClick(entryNewWidget, Qt::LeftButton);
682
    QTest::keyClicks(titleEdit, "something 5");
683
    MessageBox::setNextAnswer(MessageBox::Discard);
684
    QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Cancel), Qt::LeftButton);
685

686
    QApplication::processEvents();
687

688
    // Confirm no changed entry count
689
    QTRY_COMPARE(entryView->model()->rowCount(), 3);
690
}
691

692
void TestGui::testPasswordEntryEntropy_data()
693
{
694
    QTest::addColumn<QString>("password");
695
    QTest::addColumn<QString>("expectedStrengthLabel");
696

697
    QTest::newRow("Empty password") << ""
698
                                    << "Password Quality: Poor";
699

700
    QTest::newRow("Well-known password") << "hello"
701
                                         << "Password Quality: Poor";
702

703
    QTest::newRow("Password composed of well-known words.") << "helloworld"
704
                                                            << "Password Quality: Poor";
705

706
    QTest::newRow("Password composed of well-known words with number.") << "password1"
707
                                                                        << "Password Quality: Poor";
708

709
    QTest::newRow("Password out of small character space.") << "D0g.................."
710
                                                            << "Password Quality: Poor";
711

712
    QTest::newRow("XKCD, easy substitutions.") << "Tr0ub4dour&3"
713
                                               << "Password Quality: Poor";
714

715
    QTest::newRow("XKCD, word generator.") << "correcthorsebatterystaple"
716
                                           << "Password Quality: Weak";
717

718
    QTest::newRow("Random characters, medium length.") << "YQC3kbXbjC652dTDH"
719
                                                       << "Password Quality: Good";
720

721
    QTest::newRow("Random characters, long.") << "Bs5ZFfthWzR8DGFEjaCM6bGqhmCT4km"
722
                                              << "Password Quality: Excellent";
723

724
    QTest::newRow("Long password using Zxcvbn chunk estimation")
725
        << "quintet-tamper-kinswoman-humility-vengeful-haven-tastiness-aspire-widget-ipad-cussed-reaffirm-ladylike-"
726
           "ashamed-anatomy-daybed-jam-swear-strudel-neatness-stalemate-unbundle-flavored-relation-emergency-underrate-"
727
           "registry-getting-award-unveiled-unshaken-stagnate-cartridge-magnitude-ointment-hardener-enforced-scrubbed-"
728
           "radial-fiddling-envelope-unpaved-moisture-unused-crawlers-quartered-crushed-kangaroo-tiptop-doily"
729
        << "Password Quality: Excellent";
730

731
    QTest::newRow("Longer password above Zxcvbn threshold")
732
        << "quintet-tamper-kinswoman-humility-vengeful-haven-tastiness-aspire-widget-ipad-cussed-reaffirm-ladylike-"
733
           "ashamed-anatomy-daybed-jam-swear-strudel-neatness-stalemate-unbundle-flavored-relation-emergency-underrate-"
734
           "registry-getting-award-unveiled-unshaken-stagnate-cartridge-magnitude-ointment-hardener-enforced-scrubbed-"
735
           "radial-fiddling-envelope-unpaved-moisture-unused-crawlers-quartered-crushed-kangaroo-tiptop-doily-hefty-"
736
           "untie-fidgeting-radiance-twilight-freebase-sulphuric-parrot-decree-monotype-nautical-pout-sip-geometric-"
737
           "crunching-deviancy-festival-hacking-rage-unify-coronary-zigzagged-dwindle-possum-lilly-exhume-daringly-"
738
           "barbell-rage-animate-lapel-emporium-renounce-justifier-relieving-gauze-arrive-alive-collected-immobile-"
739
           "unleash-snowman-gift-expansion-marbles-requisite-excusable-flatness-displace-caloric-sensuous-moustache-"
740
           "sensuous-capillary-aversion-contents-cadet-giggly-amenity-peddling-spotting-drier-mooned-rudder-peroxide-"
741
           "posting-oppressor-scrabble-scorer-whomever-paprika-slapstick-said-spectacle-capture-debate-attire-emcee-"
742
           "unfocused-sympathy-doily-election-ambulance-polish-subtype-grumbling-neon-stooge-reanalyze-rockfish-"
743
           "disparate-decorated-washroom-threefold-muzzle-buckwheat-kerosene-swell-why-reprocess-correct-shady-"
744
           "impatient-slit-banshee-scrubbed-dreadful-unlocking-urologist-hurried-citable-fragment-septic-lapped-"
745
           "prankish-phantom-unpaved-moisture-unused-crawlers-quartered-crushed-kangaroo-lapel-emporium-renounce"
746
        << "Password Quality: Excellent";
747
}
748

749
void TestGui::testPasswordEntryEntropy()
750
{
751
    auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
752

753
    // Find the new entry action
754
    auto* entryNewAction = m_mainWindow->findChild<QAction*>("actionEntryNew");
755
    QVERIFY(entryNewAction->isEnabled());
756

757
    // Find the button associated with the new entry action
758
    QWidget* entryNewWidget = toolBar->widgetForAction(entryNewAction);
759
    QVERIFY(entryNewWidget->isVisible());
760
    QVERIFY(entryNewWidget->isEnabled());
761

762
    // Click the new entry button and check that we enter edit mode
763
    QTest::mouseClick(entryNewWidget, Qt::LeftButton);
764
    QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
765

766
    // Add entry "test" and confirm added
767
    auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
768
    auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
769
    QTest::keyClicks(titleEdit, "test");
770

771
    // Open the password generator
772
    auto* passwordEdit =
773
        editEntryWidget->findChild<PasswordWidget*>("passwordEdit")->findChild<QLineEdit*>("passwordEdit");
774
    QVERIFY(passwordEdit);
775
    QTest::mouseClick(passwordEdit, Qt::LeftButton);
776

777
#ifdef Q_OS_MAC
778
    QTest::keyClick(passwordEdit, Qt::Key_G, Qt::MetaModifier);
779
#else
780
    QTest::keyClick(passwordEdit, Qt::Key_G, Qt::ControlModifier);
781
#endif
782

783
    TEST_MODAL(
784
        PasswordGeneratorWidget * pwGeneratorWidget;
785
        QTRY_VERIFY(pwGeneratorWidget = m_dbWidget->findChild<PasswordGeneratorWidget*>());
786

787
        // Type in some password
788
        auto* generatedPassword =
789
            pwGeneratorWidget->findChild<PasswordWidget*>("editNewPassword")->findChild<QLineEdit*>("passwordEdit");
790
        auto* entropyLabel = pwGeneratorWidget->findChild<QLabel*>("entropyLabel");
791
        auto* strengthLabel = pwGeneratorWidget->findChild<QLabel*>("strengthLabel");
792

793
        QFETCH(QString, password);
794
        QFETCH(QString, expectedStrengthLabel);
795

796
        // Dynamically calculate entropy due to variances with zxcvbn wordlists
797
        PasswordHealth health(password);
798
        auto expectedEntropy = QString("Entropy: %1 bit").arg(QString::number(health.entropy(), 'f', 2));
799

800
        generatedPassword->setText(password);
801
        QCOMPARE(entropyLabel->text(), expectedEntropy);
802
        QCOMPARE(strengthLabel->text(), expectedStrengthLabel);
803

804
        QTest::mouseClick(generatedPassword, Qt::LeftButton);
805
        QTest::keyClick(generatedPassword, Qt::Key_Escape););
806
}
807

808
void TestGui::testDicewareEntryEntropy()
809
{
810
    auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
811

812
    // Find the new entry action
813
    auto* entryNewAction = m_mainWindow->findChild<QAction*>("actionEntryNew");
814
    QVERIFY(entryNewAction->isEnabled());
815

816
    // Find the button associated with the new entry action
817
    QWidget* entryNewWidget = toolBar->widgetForAction(entryNewAction);
818
    QVERIFY(entryNewWidget->isVisible());
819
    QVERIFY(entryNewWidget->isEnabled());
820

821
    // Click the new entry button and check that we enter edit mode
822
    QTest::mouseClick(entryNewWidget, Qt::LeftButton);
823
    QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
824

825
    // Add entry "test" and confirm added
826
    auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
827
    auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
828
    QTest::keyClicks(titleEdit, "test");
829

830
    // Open the password generator
831
    auto* passwordEdit = editEntryWidget->findChild<PasswordWidget*>()->findChild<QLineEdit*>("passwordEdit");
832
    QVERIFY(passwordEdit);
833
    QTest::mouseClick(passwordEdit, Qt::LeftButton);
834

835
#ifdef Q_OS_MAC
836
    QTest::keyClick(passwordEdit, Qt::Key_G, Qt::MetaModifier);
837
#else
838
    QTest::keyClick(passwordEdit, Qt::Key_G, Qt::ControlModifier);
839
#endif
840

841
    TEST_MODAL(
842
        PasswordGeneratorWidget * pwGeneratorWidget;
843
        QTRY_VERIFY(pwGeneratorWidget = m_dbWidget->findChild<PasswordGeneratorWidget*>());
844

845
        // Select Diceware
846
        auto* generatedPassword =
847
            pwGeneratorWidget->findChild<PasswordWidget*>("editNewPassword")->findChild<QLineEdit*>("passwordEdit");
848
        auto* tabWidget = pwGeneratorWidget->findChild<QTabWidget*>("tabWidget");
849
        auto* dicewareWidget = pwGeneratorWidget->findChild<QWidget*>("dicewareWidget");
850
        tabWidget->setCurrentWidget(dicewareWidget);
851

852
        auto* comboBoxWordList = dicewareWidget->findChild<QComboBox*>("comboBoxWordList");
853
        comboBoxWordList->setCurrentText("eff_large.wordlist");
854
        auto* spinBoxWordCount = dicewareWidget->findChild<QSpinBox*>("spinBoxWordCount");
855
        spinBoxWordCount->setValue(6);
856

857
        // Confirm a password was generated
858
        QVERIFY(!pwGeneratorWidget->getGeneratedPassword().isEmpty());
859

860
        // Verify entropy and strength
861
        auto* entropyLabel = pwGeneratorWidget->findChild<QLabel*>("entropyLabel");
862
        auto* strengthLabel = pwGeneratorWidget->findChild<QLabel*>("strengthLabel");
863
        auto* wordLengthLabel = pwGeneratorWidget->findChild<QLabel*>("charactersInPassphraseLabel");
864

865
        QTRY_COMPARE_WITH_TIMEOUT(entropyLabel->text(), QString("Entropy: 77.55 bit"), 200);
866
        QCOMPARE(strengthLabel->text(), QString("Password Quality: Good"));
867
        QCOMPARE(wordLengthLabel->text().toInt(), pwGeneratorWidget->getGeneratedPassword().size());
868

869
        QTest::mouseClick(generatedPassword, Qt::LeftButton);
870
        QTest::keyClick(generatedPassword, Qt::Key_Escape););
871
}
872

873
void TestGui::testTotp()
874
{
875
    auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
876
    auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
877

878
    QCOMPARE(entryView->model()->rowCount(), 1);
879
    QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
880
    QModelIndex item = entryView->model()->index(0, 1);
881
    Entry* entry = entryView->entryFromIndex(item);
882
    clickIndex(item, entryView, Qt::LeftButton);
883

884
    triggerAction("actionEntrySetupTotp");
885

886
    auto* setupTotpDialog = m_dbWidget->findChild<TotpSetupDialog*>("TotpSetupDialog");
887

888
    QApplication::processEvents();
889

890
    QString exampleSeed = "gezd gnbvgY 3tqojqGEZdgnb vgy3tqoJq===";
891
    QString expectedFinalSeed = exampleSeed.toUpper().remove(" ").remove("=");
892
    auto* seedEdit = setupTotpDialog->findChild<QLineEdit*>("seedEdit");
893
    seedEdit->setText("");
894
    QTest::keyClicks(seedEdit, exampleSeed);
895

896
    auto* setupTotpButtonBox = setupTotpDialog->findChild<QDialogButtonBox*>("buttonBox");
897
    QTest::mouseClick(setupTotpButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
898
    QTRY_VERIFY(!setupTotpDialog->isVisible());
899

900
    // Make sure the entryView is selected and active
901
    entryView->activateWindow();
902
    QApplication::processEvents();
903
    QTRY_VERIFY(entryView->hasFocus());
904

905
    auto* entryEditAction = m_mainWindow->findChild<QAction*>("actionEntryEdit");
906
    QWidget* entryEditWidget = toolBar->widgetForAction(entryEditAction);
907
    QVERIFY(entryEditWidget->isVisible());
908
    QVERIFY(entryEditWidget->isEnabled());
909
    QTest::mouseClick(entryEditWidget, Qt::LeftButton);
910
    QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
911

912
    auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
913
    editEntryWidget->setCurrentPage(1);
914
    auto* attrTextEdit = editEntryWidget->findChild<QPlainTextEdit*>("attributesEdit");
915
    QTest::mouseClick(editEntryWidget->findChild<QAbstractButton*>("revealAttributeButton"), Qt::LeftButton);
916
    QCOMPARE(attrTextEdit->toPlainText(), expectedFinalSeed);
917

918
    auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
919
    QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
920

921
    // Test the TOTP value
922
    triggerAction("actionEntryTotp");
923

924
    auto* totpDialog = m_dbWidget->findChild<TotpDialog*>("TotpDialog");
925
    auto* totpLabel = totpDialog->findChild<QLabel*>("totpLabel");
926

927
    QCOMPARE(totpLabel->text().replace(" ", ""), entry->totp());
928
    QTest::keyClick(totpDialog, Qt::Key_Escape);
929

930
    // Test the QR code
931
    triggerAction("actionEntryTotpQRCode");
932
    auto* qrCodeDialog = m_mainWindow->findChild<QDialog*>("entryQrCodeWidget");
933
    QVERIFY(qrCodeDialog);
934
    QVERIFY(qrCodeDialog->isVisible());
935
    auto* qrCodeWidget = qrCodeDialog->findChild<QWidget*>("squareSvgWidget");
936
    QVERIFY2(qrCodeWidget->geometry().width() == qrCodeWidget->geometry().height(), "Initial QR code is not square");
937

938
    // Test the QR code window resizing, make the dialog bigger.
939
    qrCodeDialog->setFixedSize(800, 600);
940
    QVERIFY2(qrCodeWidget->geometry().width() == qrCodeWidget->geometry().height(), "Resized QR code is not square");
941
    QTest::keyClick(qrCodeDialog, Qt::Key_Escape);
942
}
943

944
void TestGui::testSearch()
945
{
946
    // Add canned entries for consistent testing
947
    addCannedEntries();
948

949
    auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
950

951
    auto* searchWidget = toolBar->findChild<SearchWidget*>("SearchWidget");
952
    QVERIFY(searchWidget->isEnabled());
953
    auto* searchTextEdit = searchWidget->findChild<QLineEdit*>("searchEdit");
954

955
    auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
956
    QVERIFY(entryView->isVisible());
957

958
    QVERIFY(searchTextEdit->isClearButtonEnabled());
959

960
    auto* helpButton = searchWidget->findChild<QAction*>("helpIcon");
961
    auto* helpPanel = searchWidget->findChild<QWidget*>("SearchHelpWidget");
962
    QVERIFY(helpButton->isVisible());
963
    QVERIFY(!helpPanel->isVisible());
964

965
    // Enter search
966
    QTest::mouseClick(searchTextEdit, Qt::LeftButton);
967
    QTRY_VERIFY(searchTextEdit->hasFocus());
968
    // Show/Hide search help
969
    helpButton->trigger();
970
    QTRY_VERIFY(helpPanel->isVisible());
971
    QTest::mouseClick(searchTextEdit, Qt::LeftButton);
972
    QTRY_VERIFY(helpPanel->isVisible());
973
    QApplication::processEvents();
974
    helpButton->trigger();
975
    QTRY_VERIFY(!helpPanel->isVisible());
976

977
    // Need to re-activate the window after the help test
978
    m_mainWindow->activateWindow();
979

980
    // Search for "ZZZ"
981
    QTest::keyClicks(searchTextEdit, "ZZZ");
982
    QTRY_COMPARE(searchTextEdit->text(), QString("ZZZ"));
983
    QTRY_VERIFY(m_dbWidget->isSearchActive());
984
    QTRY_COMPARE(entryView->model()->rowCount(), 0);
985
    // Press the search clear button
986
    searchTextEdit->clear();
987
    QTRY_VERIFY(searchTextEdit->text().isEmpty());
988
    QTRY_VERIFY(searchTextEdit->hasFocus());
989

990
    // Test tag search
991
    searchTextEdit->clear();
992
    QTest::keyClicks(searchTextEdit, "tag: testTag");
993
    QTRY_VERIFY(m_dbWidget->isSearchActive());
994
    QTRY_COMPARE(entryView->model()->rowCount(), 1);
995

996
    searchTextEdit->clear();
997
    QTRY_VERIFY(searchTextEdit->text().isEmpty());
998
    QTRY_VERIFY(searchTextEdit->hasFocus());
999
    // Escape clears searchedit and retains focus
1000
    QTest::keyClicks(searchTextEdit, "ZZZ");
1001
    QTest::keyClick(searchTextEdit, Qt::Key_Escape);
1002
    QTRY_VERIFY(searchTextEdit->text().isEmpty());
1003
    QTRY_VERIFY(searchTextEdit->hasFocus());
1004
    QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
1005
    // Search for "some"
1006
    QTest::keyClicks(searchTextEdit, "some");
1007
    QTRY_VERIFY(m_dbWidget->isSearchActive());
1008
    QTRY_COMPARE(entryView->model()->rowCount(), 3);
1009
    // Search for "someTHING"
1010
    QTest::keyClicks(searchTextEdit, "THING");
1011
    QTRY_COMPARE(entryView->model()->rowCount(), 2);
1012
    // Press Down to focus on the entry view
1013
    QTest::keyClick(searchTextEdit, Qt::Key_Right, Qt::ControlModifier);
1014
    QTRY_VERIFY(searchTextEdit->hasFocus());
1015
    QTest::keyClick(searchTextEdit, Qt::Key_Down);
1016
    QTRY_VERIFY(entryView->hasFocus());
1017
    auto* searchedEntry = entryView->currentEntry();
1018
    // Restore focus using F3 key and search text selection
1019
    QTest::keyClick(m_mainWindow.data(), Qt::Key_F3);
1020
    QTRY_VERIFY(searchTextEdit->hasFocus());
1021
    QTRY_COMPARE(searchTextEdit->selectedText(), QString("someTHING"));
1022

1023
    searchedEntry->setPassword("password");
1024
    QClipboard* clipboard = QApplication::clipboard();
1025

1026
    // Attempt password copy with selected test (should fail)
1027
    QTest::keyClick(searchTextEdit, Qt::Key_C, Qt::ControlModifier);
1028
    QVERIFY(clipboard->text() != searchedEntry->password());
1029
    // Deselect text and confirm password copies
1030
    QTest::mouseClick(searchTextEdit, Qt::LeftButton);
1031
    QTRY_VERIFY(searchTextEdit->selectedText().isEmpty());
1032
    QTRY_VERIFY(searchTextEdit->hasFocus());
1033
    QTest::keyClick(searchTextEdit, Qt::Key_C, Qt::ControlModifier);
1034
    QCOMPARE(searchedEntry->password(), clipboard->text());
1035
    // Ensure Down focuses on entry view when search text is selected
1036
    QTest::keyClick(searchTextEdit, Qt::Key_A, Qt::ControlModifier);
1037
    QTest::keyClick(searchTextEdit, Qt::Key_Down);
1038
    QTRY_VERIFY(entryView->hasFocus());
1039
    QCOMPARE(entryView->currentEntry(), searchedEntry);
1040
    // Test that password copies with entry focused
1041
    QTest::keyClick(entryView, Qt::Key_C, Qt::ControlModifier);
1042
    QCOMPARE(searchedEntry->password(), clipboard->text());
1043
    // Refocus back to search edit
1044
    QTest::mouseClick(searchTextEdit, Qt::LeftButton);
1045
    QTRY_VERIFY(searchTextEdit->hasFocus());
1046
    // Test that password does not copy
1047
    searchTextEdit->selectAll();
1048
    QTest::keyClick(searchTextEdit, Qt::Key_C, Qt::ControlModifier);
1049
    QTRY_COMPARE(clipboard->text(), QString("someTHING"));
1050

1051
    // Test case sensitive search
1052
    searchWidget->setCaseSensitive(true);
1053
    QTRY_COMPARE(entryView->model()->rowCount(), 0);
1054
    searchWidget->setCaseSensitive(false);
1055
    QTRY_COMPARE(entryView->model()->rowCount(), 2);
1056

1057
    // Test group search
1058
    searchWidget->setLimitGroup(false);
1059
    GroupView* groupView = m_dbWidget->findChild<GroupView*>("groupView");
1060
    QCOMPARE(groupView->currentGroup(), m_db->rootGroup());
1061
    QModelIndex rootGroupIndex = groupView->model()->index(0, 0);
1062
    clickIndex(groupView->model()->index(0, 0, rootGroupIndex), groupView, Qt::LeftButton);
1063
    QCOMPARE(groupView->currentGroup()->name(), QString("General"));
1064
    // Selecting a group should cancel search
1065
    QTRY_COMPARE(entryView->model()->rowCount(), 0);
1066
    // Restore search
1067
    QTest::keyClick(m_mainWindow.data(), Qt::Key_F, Qt::ControlModifier);
1068
    QTest::keyClicks(searchTextEdit, "someTHING");
1069
    QTRY_COMPARE(entryView->model()->rowCount(), 2);
1070
    // Enable group limiting
1071
    searchWidget->setLimitGroup(true);
1072
    QTRY_COMPARE(entryView->model()->rowCount(), 0);
1073
    // Selecting another group should NOT cancel search
1074
    clickIndex(rootGroupIndex, groupView, Qt::LeftButton);
1075
    QCOMPARE(groupView->currentGroup(), m_db->rootGroup());
1076
    QTRY_COMPARE(entryView->model()->rowCount(), 2);
1077

1078
    // reset
1079
    searchWidget->setLimitGroup(false);
1080
    clickIndex(rootGroupIndex, groupView, Qt::LeftButton);
1081
    QCOMPARE(groupView->currentGroup(), m_db->rootGroup());
1082
    QVERIFY(!m_dbWidget->isSearchActive());
1083

1084
    // check if first entry is selected after search
1085
    QTest::keyClicks(searchTextEdit, "some");
1086
    QTRY_VERIFY(m_dbWidget->isSearchActive());
1087
    QTRY_COMPARE(entryView->selectedEntries().length(), 1);
1088
    QModelIndex index_current = entryView->indexFromEntry(entryView->currentEntry());
1089
    QTRY_COMPARE(index_current.row(), 0);
1090

1091
    // Try to edit the first entry from the search view
1092
    // Refocus back to search edit
1093
    QTest::mouseClick(searchTextEdit, Qt::LeftButton);
1094
    QTRY_VERIFY(searchTextEdit->hasFocus());
1095
    QTest::keyClicks(searchTextEdit, "someTHING");
1096
    QTRY_VERIFY(m_dbWidget->isSearchActive());
1097

1098
    QModelIndex item = entryView->model()->index(0, 1);
1099
    Entry* entry = entryView->entryFromIndex(item);
1100
    QTest::keyClick(searchTextEdit, Qt::Key_Return);
1101
    QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
1102

1103
    // Perform the edit and save it
1104
    EditEntryWidget* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
1105
    QLineEdit* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
1106
    QString origTitle = titleEdit->text();
1107
    QTest::keyClicks(titleEdit, "_edited");
1108
    QDialogButtonBox* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
1109
    QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
1110

1111
    // Confirm the edit was made and we are back in search mode
1112
    QTRY_VERIFY(m_dbWidget->isSearchActive());
1113
    QCOMPARE(entry->title(), origTitle.append("_edited"));
1114

1115
    // Cancel search, should return to normal view
1116
    QTest::keyClick(m_mainWindow.data(), Qt::Key_Escape);
1117
    QTRY_COMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
1118
}
1119

1120
void TestGui::testDeleteEntry()
1121
{
1122
    // Add canned entries for consistent testing
1123
    addCannedEntries();
1124
    checkStatusBarText("4 Ent");
1125

1126
    auto* groupView = m_dbWidget->findChild<GroupView*>("groupView");
1127
    auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
1128
    auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
1129
    auto* entryDeleteAction = m_mainWindow->findChild<QAction*>("actionEntryDelete");
1130
    QWidget* entryDeleteWidget = toolBar->widgetForAction(entryDeleteAction);
1131
    entryView->setFocus();
1132

1133
    // Move one entry to the recycling bin
1134
    QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
1135
    clickIndex(entryView->model()->index(1, 1), entryView, Qt::LeftButton);
1136
    QVERIFY(entryDeleteWidget->isVisible());
1137
    QVERIFY(entryDeleteWidget->isEnabled());
1138
    QVERIFY(!m_db->metadata()->recycleBin());
1139

1140
    // Test with confirmation dialog
1141
    if (!config()->get(Config::Security_NoConfirmMoveEntryToRecycleBin).toBool()) {
1142
        MessageBox::setNextAnswer(MessageBox::Move);
1143
        QTest::mouseClick(entryDeleteWidget, Qt::LeftButton);
1144

1145
        QCOMPARE(entryView->model()->rowCount(), 3);
1146
        QCOMPARE(m_db->metadata()->recycleBin()->entries().size(), 1);
1147
    } else {
1148
        // no confirm dialog
1149
        QTest::mouseClick(entryDeleteWidget, Qt::LeftButton);
1150
        QCOMPARE(entryView->model()->rowCount(), 3);
1151
        QCOMPARE(m_db->metadata()->recycleBin()->entries().size(), 1);
1152
    }
1153

1154
    checkStatusBarText("3 Ent");
1155

1156
    // Select multiple entries and move them to the recycling bin
1157
    clickIndex(entryView->model()->index(1, 1), entryView, Qt::LeftButton);
1158
    clickIndex(entryView->model()->index(2, 1), entryView, Qt::LeftButton, Qt::ControlModifier);
1159
    QCOMPARE(entryView->selectionModel()->selectedRows().size(), 2);
1160

1161
    if (!config()->get(Config::Security_NoConfirmMoveEntryToRecycleBin).toBool()) {
1162
        MessageBox::setNextAnswer(MessageBox::Cancel);
1163
        QTest::mouseClick(entryDeleteWidget, Qt::LeftButton);
1164
        QCOMPARE(entryView->model()->rowCount(), 3);
1165
        QCOMPARE(m_db->metadata()->recycleBin()->entries().size(), 1);
1166

1167
        MessageBox::setNextAnswer(MessageBox::Move);
1168
        QTest::mouseClick(entryDeleteWidget, Qt::LeftButton);
1169
        QCOMPARE(entryView->model()->rowCount(), 1);
1170
        QCOMPARE(m_db->metadata()->recycleBin()->entries().size(), 3);
1171
    } else {
1172
        QTest::mouseClick(entryDeleteWidget, Qt::LeftButton);
1173
        QCOMPARE(entryView->model()->rowCount(), 1);
1174
        QCOMPARE(m_db->metadata()->recycleBin()->entries().size(), 3);
1175
    }
1176

1177
    // Go to the recycling bin
1178
    QCOMPARE(groupView->currentGroup(), m_db->rootGroup());
1179
    QModelIndex rootGroupIndex = groupView->model()->index(0, 0);
1180
    clickIndex(groupView->model()->index(groupView->model()->rowCount(rootGroupIndex) - 1, 0, rootGroupIndex),
1181
               groupView,
1182
               Qt::LeftButton);
1183
    QCOMPARE(groupView->currentGroup()->name(), m_db->metadata()->recycleBin()->name());
1184

1185
    // Delete one entry from the bin
1186
    clickIndex(entryView->model()->index(0, 1), entryView, Qt::LeftButton);
1187
    MessageBox::setNextAnswer(MessageBox::Cancel);
1188
    QTest::mouseClick(entryDeleteWidget, Qt::LeftButton);
1189
    QCOMPARE(entryView->model()->rowCount(), 3);
1190
    QCOMPARE(m_db->metadata()->recycleBin()->entries().size(), 3);
1191

1192
    MessageBox::setNextAnswer(MessageBox::Delete);
1193
    QTest::mouseClick(entryDeleteWidget, Qt::LeftButton);
1194
    QCOMPARE(entryView->model()->rowCount(), 2);
1195
    QCOMPARE(m_db->metadata()->recycleBin()->entries().size(), 2);
1196

1197
    // Select the remaining entries and delete them
1198
    clickIndex(entryView->model()->index(0, 1), entryView, Qt::LeftButton);
1199
    clickIndex(entryView->model()->index(1, 1), entryView, Qt::LeftButton, Qt::ControlModifier);
1200
    MessageBox::setNextAnswer(MessageBox::Delete);
1201
    QTest::mouseClick(entryDeleteWidget, Qt::LeftButton);
1202
    QCOMPARE(entryView->model()->rowCount(), 0);
1203
    QCOMPARE(m_db->metadata()->recycleBin()->entries().size(), 0);
1204

1205
    // Ensure the entry preview widget shows the recycling group since all entries are deleted
1206
    auto* previewWidget = m_dbWidget->findChild<EntryPreviewWidget*>("previewWidget");
1207
    QVERIFY(previewWidget);
1208
    auto* groupTitleLabel = previewWidget->findChild<QLabel*>("groupTitleLabel");
1209
    QVERIFY(groupTitleLabel);
1210

1211
    QTRY_VERIFY(groupTitleLabel->isVisible());
1212
    QVERIFY(groupTitleLabel->text().contains(m_db->metadata()->recycleBin()->name()));
1213

1214
    // Go back to the root group
1215
    clickIndex(groupView->model()->index(0, 0), groupView, Qt::LeftButton);
1216
    QCOMPARE(groupView->currentGroup(), m_db->rootGroup());
1217
}
1218

1219
void TestGui::testCloneEntry()
1220
{
1221
    auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
1222
    entryView->setFocus();
1223

1224
    QCOMPARE(entryView->model()->rowCount(), 1);
1225

1226
    QModelIndex item = entryView->model()->index(0, 1);
1227
    Entry* entryOrg = entryView->entryFromIndex(item);
1228
    clickIndex(item, entryView, Qt::LeftButton);
1229

1230
    triggerAction("actionEntryClone");
1231

1232
    auto* cloneDialog = m_dbWidget->findChild<CloneDialog*>("CloneDialog");
1233
    auto* cloneButtonBox = cloneDialog->findChild<QDialogButtonBox*>("buttonBox");
1234
    QTest::mouseClick(cloneButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
1235

1236
    QCOMPARE(entryView->model()->rowCount(), 2);
1237
    Entry* entryClone = entryView->entryFromIndex(entryView->model()->index(1, 1));
1238
    QVERIFY(entryOrg->uuid() != entryClone->uuid());
1239
    QCOMPARE(entryClone->title(), entryOrg->title() + QString(" - Clone"));
1240
    QVERIFY(m_dbWidget->currentSelectedEntry()->uuid() == entryClone->uuid());
1241
}
1242

1243
void TestGui::testEntryPlaceholders()
1244
{
1245
    auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
1246
    auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
1247

1248
    // Find the new entry action
1249
    auto* entryNewAction = m_mainWindow->findChild<QAction*>("actionEntryNew");
1250
    QVERIFY(entryNewAction->isEnabled());
1251

1252
    // Find the button associated with the new entry action
1253
    QWidget* entryNewWidget = toolBar->widgetForAction(entryNewAction);
1254
    QVERIFY(entryNewWidget->isVisible());
1255
    QVERIFY(entryNewWidget->isEnabled());
1256

1257
    // Click the new entry button and check that we enter edit mode
1258
    QTest::mouseClick(entryNewWidget, Qt::LeftButton);
1259
    QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
1260

1261
    // Add entry "test" and confirm added
1262
    auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
1263
    auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
1264
    QTest::keyClicks(titleEdit, "test");
1265
    QComboBox* usernameComboBox = editEntryWidget->findChild<QComboBox*>("usernameComboBox");
1266
    QTest::keyClicks(usernameComboBox, "john");
1267
    QLineEdit* urlEdit = editEntryWidget->findChild<QLineEdit*>("urlEdit");
1268
    QTest::keyClicks(urlEdit, "{TITLE}.{USERNAME}");
1269
    auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
1270
    QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
1271

1272
    QCOMPARE(entryView->model()->rowCount(), 2);
1273

1274
    QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
1275
    QModelIndex item = entryView->model()->index(1, 1);
1276
    Entry* entry = entryView->entryFromIndex(item);
1277

1278
    QCOMPARE(entry->title(), QString("test"));
1279
    QCOMPARE(entry->url(), QString("{TITLE}.{USERNAME}"));
1280

1281
    // Test password copy
1282
    QClipboard* clipboard = QApplication::clipboard();
1283
    m_dbWidget->copyURL();
1284
    QTRY_COMPARE(clipboard->text(), QString("test.john"));
1285
}
1286

1287
void TestGui::testDragAndDropEntry()
1288
{
1289
    auto entryView = m_dbWidget->findChild<EntryView*>("entryView");
1290
    auto groupView = m_dbWidget->findChild<GroupView*>("groupView");
1291
    auto groupModel = qobject_cast<GroupModel*>(groupView->model());
1292

1293
    QModelIndex sourceIndex = entryView->model()->index(0, 1);
1294
    QModelIndex targetIndex = groupModel->index(0, 0, groupModel->index(0, 0));
1295
    QVERIFY(sourceIndex.isValid());
1296
    QVERIFY(targetIndex.isValid());
1297
    auto targetGroup = groupModel->groupFromIndex(targetIndex);
1298

1299
    QMimeData mimeData;
1300
    QByteArray encoded;
1301
    QDataStream stream(&encoded, QIODevice::WriteOnly);
1302

1303
    auto entry = entryView->entryFromIndex(sourceIndex);
1304
    stream << entry->group()->database()->uuid() << entry->uuid();
1305
    mimeData.setData("application/x-keepassx-entry", encoded);
1306

1307
    // Test Copy, UUID should change, history remain
1308
    QVERIFY(groupModel->dropMimeData(&mimeData, Qt::CopyAction, -1, 0, targetIndex));
1309
    // Find the copied entry
1310
    auto newEntry = targetGroup->findEntryByPath(entry->title());
1311
    QVERIFY(newEntry);
1312
    QVERIFY(entry->uuid() != newEntry->uuid());
1313
    QCOMPARE(entry->historyItems().count(), newEntry->historyItems().count());
1314

1315
    encoded.clear();
1316
    entry = entryView->entryFromIndex(sourceIndex);
1317
    auto history = entry->historyItems().count();
1318
    auto uuid = entry->uuid();
1319
    stream << entry->group()->database()->uuid() << entry->uuid();
1320
    mimeData.setData("application/x-keepassx-entry", encoded);
1321

1322
    // Test Move, entry pointer should remain the same
1323
    QCOMPARE(entry->group()->name(), QString("NewDatabase"));
1324
    QVERIFY(groupModel->dropMimeData(&mimeData, Qt::MoveAction, -1, 0, targetIndex));
1325
    QCOMPARE(entry->group()->name(), QString("General"));
1326
    QCOMPARE(entry->uuid(), uuid);
1327
    QCOMPARE(entry->historyItems().count(), history);
1328
}
1329

1330
void TestGui::testDragAndDropGroup()
1331
{
1332
    QAbstractItemModel* groupModel = m_dbWidget->findChild<GroupView*>("groupView")->model();
1333
    QModelIndex rootIndex = groupModel->index(0, 0);
1334

1335
    dragAndDropGroup(groupModel->index(0, 0, rootIndex), groupModel->index(1, 0, rootIndex), -1, true, "Windows", 0);
1336

1337
    // dropping parent on child is supposed to fail
1338
    dragAndDropGroup(groupModel->index(0, 0, rootIndex),
1339
                     groupModel->index(0, 0, groupModel->index(0, 0, rootIndex)),
1340
                     -1,
1341
                     false,
1342
                     "NewDatabase",
1343
                     0);
1344

1345
    dragAndDropGroup(groupModel->index(1, 0, rootIndex), rootIndex, 0, true, "NewDatabase", 0);
1346

1347
    dragAndDropGroup(groupModel->index(0, 0, rootIndex), rootIndex, -1, true, "NewDatabase", 4);
1348
}
1349

1350
void TestGui::testSaveAs()
1351
{
1352
    QFileInfo fileInfo(m_dbFilePath);
1353
    QDateTime lastModified = fileInfo.lastModified();
1354

1355
    m_db->metadata()->setName("testSaveAs");
1356

1357
    // open temporary file so it creates a filename
1358
    TemporaryFile tmpFile;
1359
    QVERIFY(tmpFile.open());
1360
    QString tmpFileName = tmpFile.fileName();
1361
    tmpFile.remove();
1362

1363
    fileDialog()->setNextFileName(tmpFileName);
1364

1365
    triggerAction("actionDatabaseSaveAs");
1366

1367
    QCOMPARE(m_tabWidget->tabText(m_tabWidget->currentIndex()), QString("testSaveAs"));
1368

1369
    checkDatabase(tmpFileName);
1370

1371
    fileInfo.refresh();
1372
    QCOMPARE(fileInfo.lastModified(), lastModified);
1373
    tmpFile.remove();
1374
}
1375

1376
void TestGui::testSaveBackup()
1377
{
1378
    m_db->metadata()->setName("testSaveBackup");
1379

1380
    QFileInfo fileInfo(m_dbFilePath);
1381
    QDateTime lastModified = fileInfo.lastModified();
1382

1383
    // open temporary file so it creates a filename
1384
    TemporaryFile tmpFile;
1385
    QVERIFY(tmpFile.open());
1386
    QString tmpFileName = tmpFile.fileName();
1387
    tmpFile.remove();
1388

1389
    // wait for modified timer
1390
    QTRY_COMPARE(m_tabWidget->tabText(m_tabWidget->currentIndex()), QString("testSaveBackup*"));
1391

1392
    fileDialog()->setNextFileName(tmpFileName);
1393

1394
    triggerAction("actionDatabaseSaveBackup");
1395

1396
    QTRY_COMPARE(m_tabWidget->tabText(m_tabWidget->currentIndex()), QString("testSaveBackup*"));
1397

1398
    checkDatabase(tmpFileName);
1399

1400
    fileInfo.refresh();
1401
    QCOMPARE(fileInfo.lastModified(), lastModified);
1402
    tmpFile.remove();
1403
}
1404

1405
void TestGui::testSave()
1406
{
1407
    // Make a modification to the database then save
1408
    m_db->metadata()->setName("testSave");
1409
    checkSaveDatabase();
1410
}
1411

1412
void TestGui::testSaveBackupPath_data()
1413
{
1414
    QTest::addColumn<QString>("backupFilePathPattern");
1415
    QTest::addColumn<QString>("expectedBackupFile");
1416

1417
    // Absolute paths should remain absolute
1418
    TemporaryFile tmpFile;
1419
    QVERIFY(tmpFile.open());
1420
    tmpFile.remove();
1421

1422
    QTest::newRow("Absolute backup path") << tmpFile.fileName() << tmpFile.fileName();
1423
    // relative paths should be resolved to database parent directory
1424
    QTest::newRow("Relative backup path (implicit)") << "other_dir/test.old.kdbx"
1425
                                                     << "other_dir/test.old.kdbx";
1426
    QTest::newRow("Relative backup path (explicit)") << "./other_dir2/test2.old.kdbx"
1427
                                                     << "other_dir2/test2.old.kdbx";
1428

1429
    QTest::newRow("Path with placeholders") << "{DB_FILENAME}.old.kdbx"
1430
                                            << "KeePassXC.old.kdbx";
1431
    // empty path should be replaced with default pattern
1432
    QTest::newRow("Empty path") << QString("") << config()->getDefault(Config::BackupFilePathPattern).toString();
1433
    // {DB_FILENAME} should be replaced with database filename
1434
    QTest::newRow("") << "{DB_FILENAME}_.old.kdbx"
1435
                      << "{DB_FILENAME}_.old.kdbx";
1436
}
1437

1438
void TestGui::testSaveBackupPath()
1439
{
1440
    /**
1441
     * Tests that the backupFilePathPattern config entry is respected. We do not test patterns like {TIME} etc here
1442
     * as this is done in a separate test case. We do however check {DB_FILENAME} as this is a feature of the
1443
     * performBackup() function.
1444
     */
1445

1446
    // Get test data
1447
    QFETCH(QString, backupFilePathPattern);
1448
    QFETCH(QString, expectedBackupFile);
1449

1450
    // Enable automatic backups
1451
    config()->set(Config::BackupBeforeSave, true);
1452
    config()->set(Config::BackupFilePathPattern, backupFilePathPattern);
1453

1454
    // Replace placeholders and resolve relative paths. This cannot be done in the _data() function as the
1455
    // db path/filename is not known yet
1456
    auto dbFileInfo = QFileInfo(m_dbFilePath);
1457
    if (!QDir::isAbsolutePath(expectedBackupFile)) {
1458
        expectedBackupFile = QDir(dbFileInfo.absolutePath()).absoluteFilePath(expectedBackupFile);
1459
    }
1460
    expectedBackupFile.replace("{DB_FILENAME}", dbFileInfo.completeBaseName());
1461

1462
    // Save a modified database
1463
    auto prevName = m_db->metadata()->name();
1464
    m_db->metadata()->setName("testBackupPathPattern");
1465
    checkSaveDatabase();
1466

1467
    // Test that the backup file has the previous database name
1468
    checkDatabase(expectedBackupFile, prevName);
1469

1470
    // Clean up
1471
    QFile(expectedBackupFile).remove();
1472
}
1473

1474
void TestGui::testDatabaseSettings()
1475
{
1476
    m_db->metadata()->setName("testDatabaseSettings");
1477
    triggerAction("actionDatabaseSettings");
1478
    auto* dbSettingsDialog = m_dbWidget->findChild<QWidget*>("databaseSettingsDialog");
1479
    auto* dbSettingsCategoryList = dbSettingsDialog->findChild<CategoryListWidget*>("categoryList");
1480
    auto* dbSettingsStackedWidget = dbSettingsDialog->findChild<QStackedWidget*>("stackedWidget");
1481
    auto* autosaveDelayCheckBox = dbSettingsDialog->findChild<QCheckBox*>("autosaveDelayCheckBox");
1482
    auto* autosaveDelaySpinBox = dbSettingsDialog->findChild<QSpinBox*>("autosaveDelaySpinBox");
1483
    auto* dbSettingsButtonBox = dbSettingsDialog->findChild<QDialogButtonBox*>("buttonBox");
1484
    int autosaveDelayTestValue = 2;
1485

1486
    dbSettingsCategoryList->setCurrentCategory(1); // go into security category
1487
    dbSettingsStackedWidget->findChild<QTabWidget*>()->setCurrentIndex(1); // go into encryption tab
1488

1489
    auto encryptionSettings = dbSettingsDialog->findChild<QTabWidget*>("encryptionSettingsTabWidget");
1490
    auto advancedTab = encryptionSettings->findChild<QWidget*>("advancedTab");
1491
    encryptionSettings->setCurrentWidget(advancedTab);
1492

1493
    QApplication::processEvents();
1494

1495
    auto transformRoundsSpinBox = advancedTab->findChild<QSpinBox*>("transformRoundsSpinBox");
1496
    QVERIFY(transformRoundsSpinBox);
1497
    QVERIFY(transformRoundsSpinBox->isVisible());
1498

1499
    transformRoundsSpinBox->setValue(123456);
1500
    QTest::keyClick(transformRoundsSpinBox, Qt::Key_Enter);
1501
    QTRY_COMPARE(m_db->kdf()->rounds(), 123456);
1502

1503
    // test disable and default values for maximum history items and size
1504
    triggerAction("actionDatabaseSettings");
1505
    auto* historyMaxItemsCheckBox = dbSettingsDialog->findChild<QCheckBox*>("historyMaxItemsCheckBox");
1506
    auto* historyMaxItemsSpinBox = dbSettingsDialog->findChild<QSpinBox*>("historyMaxItemsSpinBox");
1507
    auto* historyMaxSizeCheckBox = dbSettingsDialog->findChild<QCheckBox*>("historyMaxSizeCheckBox");
1508
    auto* historyMaxSizeSpinBox = dbSettingsDialog->findChild<QSpinBox*>("historyMaxSizeSpinBox");
1509
    // test defaults
1510
    QCOMPARE(historyMaxItemsSpinBox->value(), Metadata::DefaultHistoryMaxItems);
1511
    QCOMPARE(historyMaxSizeSpinBox->value(), qRound(Metadata::DefaultHistoryMaxSize / qreal(1024 * 1024)));
1512
    // disable and test setting as well
1513
    historyMaxItemsCheckBox->setChecked(false);
1514
    historyMaxSizeCheckBox->setChecked(false);
1515
    QTest::mouseClick(dbSettingsButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
1516
    QTRY_COMPARE(m_db->metadata()->historyMaxItems(), -1);
1517
    QTRY_COMPARE(m_db->metadata()->historyMaxSize(), -1);
1518
    // then open to check the saved disabled state in gui
1519
    triggerAction("actionDatabaseSettings");
1520
    QCOMPARE(historyMaxItemsCheckBox->isChecked(), false);
1521
    QCOMPARE(historyMaxSizeCheckBox->isChecked(), false);
1522
    QTest::mouseClick(dbSettingsButtonBox->button(QDialogButtonBox::Cancel), Qt::LeftButton);
1523

1524
    // Test loading default values and setting autosaveDelay
1525
    triggerAction("actionDatabaseSettings");
1526
    QVERIFY(autosaveDelayCheckBox->isChecked() == false);
1527
    autosaveDelayCheckBox->toggle();
1528
    autosaveDelaySpinBox->setValue(autosaveDelayTestValue);
1529
    QTest::mouseClick(dbSettingsButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
1530
    QTRY_COMPARE(m_db->metadata()->autosaveDelayMin(), autosaveDelayTestValue);
1531

1532
    checkSaveDatabase();
1533

1534
    // Test loading autosaveDelay non-default values
1535
    triggerAction("actionDatabaseSettings");
1536
    QTRY_COMPARE(autosaveDelayCheckBox->isChecked(), true);
1537
    QTRY_COMPARE(autosaveDelaySpinBox->value(), autosaveDelayTestValue);
1538
    QTest::mouseClick(dbSettingsButtonBox->button(QDialogButtonBox::Cancel), Qt::LeftButton);
1539

1540
    // test autosave delay
1541

1542
    // 1 init
1543
    config()->set(Config::AutoSaveAfterEveryChange, true);
1544
    QSignalSpy writeDbSignalSpy(m_db.data(), &Database::databaseSaved);
1545

1546
    // 2 create new entries
1547

1548
    // 2.a) Click the new entry button and set the title
1549
    auto* entryNewAction = m_mainWindow->findChild<QAction*>("actionEntryNew");
1550
    QVERIFY(entryNewAction->isEnabled());
1551

1552
    auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
1553
    QVERIFY(toolBar);
1554

1555
    QWidget* entryNewWidget = toolBar->widgetForAction(entryNewAction);
1556

1557
    QTest::mouseClick(entryNewWidget, Qt::LeftButton);
1558
    QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
1559

1560
    auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
1561
    QVERIFY(editEntryWidget);
1562
    auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
1563
    QVERIFY(titleEdit);
1564

1565
    QTest::keyClicks(titleEdit, "Test autosaveDelay 1");
1566

1567
    // 2.b) Save changes
1568
    editEntryWidget->setCurrentPage(0);
1569
    auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
1570
    QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
1571

1572
    // 2.c) Make sure file was not modified yet
1573
    Tools::wait(150); // due to modify timer
1574
    QTRY_COMPARE(writeDbSignalSpy.count(), 0);
1575

1576
    // 2.d) Create second entry to test delay timer reset
1577
    QTest::mouseClick(entryNewWidget, Qt::LeftButton);
1578
    QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
1579
    QTest::keyClicks(titleEdit, "Test autosaveDelay 2");
1580

1581
    // 2.e) Save changes
1582
    editEntryWidget->setCurrentPage(0);
1583
    editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
1584
    QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
1585

1586
    // 3 Double check both true negative and true positive
1587
    // 3.a) Test unmodified prior to delay timeout
1588
    Tools::wait(150); // due to modify timer
1589
    QTRY_COMPARE(writeDbSignalSpy.count(), 0);
1590

1591
    // 3.b) Test modification time after expected
1592
    m_dbWidget->triggerAutosaveTimer();
1593
    QTRY_COMPARE(writeDbSignalSpy.count(), 1);
1594

1595
    // 4 Test no delay when disabled autosave or autosaveDelay
1596
    // 4.a) create new entry
1597
    QTest::mouseClick(entryNewWidget, Qt::LeftButton);
1598
    QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
1599
    QTest::keyClicks(titleEdit, "Test autosaveDelay 3");
1600

1601
    // 4.b) Save changes
1602
    editEntryWidget->setCurrentPage(0);
1603
    editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
1604
    QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
1605

1606
    // 4.c) Start timer
1607
    Tools::wait(150); // due to modify timer
1608

1609
    // 4.d) Disable autosave
1610
    config()->set(Config::AutoSaveAfterEveryChange, false);
1611

1612
    // 4.e) Make sure changes are not saved
1613
    m_dbWidget->triggerAutosaveTimer();
1614
    QTRY_COMPARE(writeDbSignalSpy.count(), 1);
1615

1616
    // 4.f) Repeat for autosaveDelay
1617
    config()->set(Config::AutoSaveAfterEveryChange, true);
1618
    QTest::mouseClick(entryNewWidget, Qt::LeftButton);
1619
    QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
1620
    QTest::keyClicks(titleEdit, "Test autosaveDelay 4");
1621
    editEntryWidget->setCurrentPage(0);
1622
    editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
1623
    QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
1624
    Tools::wait(150); // due to modify timer
1625
    m_db->metadata()->setAutosaveDelayMin(0);
1626

1627
    // 4.g) Make sure changes are not saved
1628
    m_dbWidget->triggerAutosaveTimer();
1629
    QTRY_COMPARE(writeDbSignalSpy.count(), 1);
1630

1631
    // 5 Cleanup
1632
    config()->set(Config::AutoSaveAfterEveryChange, false);
1633
}
1634

1635
void TestGui::testDatabaseLocking()
1636
{
1637
    QString origDbName = m_tabWidget->tabText(0);
1638

1639
    MessageBox::setNextAnswer(MessageBox::Cancel);
1640
    triggerAction("actionLockAllDatabases");
1641

1642
    QCOMPARE(m_tabWidget->tabText(0), origDbName + " [Locked]");
1643

1644
    auto* actionDatabaseMerge = m_mainWindow->findChild<QAction*>("actionDatabaseMerge", Qt::FindChildrenRecursively);
1645
    QCOMPARE(actionDatabaseMerge->isEnabled(), false);
1646
    auto* actionDatabaseSave = m_mainWindow->findChild<QAction*>("actionDatabaseSave", Qt::FindChildrenRecursively);
1647
    QCOMPARE(actionDatabaseSave->isEnabled(), false);
1648

1649
    DatabaseWidget* dbWidget = m_tabWidget->currentDatabaseWidget();
1650
    QVERIFY(dbWidget->isLocked());
1651
    auto* unlockDatabaseWidget = dbWidget->findChild<QWidget*>("databaseOpenWidget");
1652
    QWidget* editPassword =
1653
        unlockDatabaseWidget->findChild<PasswordWidget*>("editPassword")->findChild<QLineEdit*>("passwordEdit");
1654
    QVERIFY(editPassword);
1655

1656
    QTest::keyClicks(editPassword, "a");
1657
    QTest::keyClick(editPassword, Qt::Key_Enter);
1658

1659
    QVERIFY(!dbWidget->isLocked());
1660
    QCOMPARE(m_tabWidget->tabText(0), origDbName);
1661

1662
    actionDatabaseMerge = m_mainWindow->findChild<QAction*>("actionDatabaseMerge", Qt::FindChildrenRecursively);
1663
    QCOMPARE(actionDatabaseMerge->isEnabled(), true);
1664
}
1665

1666
void TestGui::testDragAndDropKdbxFiles()
1667
{
1668
    const int openedDatabasesCount = m_tabWidget->count();
1669

1670
    const QString badDatabaseFilePath(QString(KEEPASSX_TEST_DATA_DIR).append("/NotDatabase.notkdbx"));
1671
    const QString goodDatabaseFilePath(QString(KEEPASSX_TEST_DATA_DIR).append("/NewDatabase.kdbx"));
1672

1673
    QMimeData badMimeData;
1674
    badMimeData.setUrls({QUrl::fromLocalFile(badDatabaseFilePath)});
1675
    QDragEnterEvent badDragEvent(QPoint(1, 1), Qt::LinkAction, &badMimeData, Qt::LeftButton, Qt::NoModifier);
1676
    qApp->notify(m_mainWindow.data(), &badDragEvent);
1677
    QCOMPARE(badDragEvent.isAccepted(), false);
1678

1679
    QDropEvent badDropEvent(QPoint(1, 1), Qt::LinkAction, &badMimeData, Qt::LeftButton, Qt::NoModifier);
1680
    qApp->notify(m_mainWindow.data(), &badDropEvent);
1681
    QCOMPARE(badDropEvent.isAccepted(), false);
1682

1683
    QCOMPARE(m_tabWidget->count(), openedDatabasesCount);
1684

1685
    QMimeData goodMimeData;
1686
    goodMimeData.setUrls({QUrl::fromLocalFile(goodDatabaseFilePath)});
1687
    QDragEnterEvent goodDragEvent(QPoint(1, 1), Qt::LinkAction, &goodMimeData, Qt::LeftButton, Qt::NoModifier);
1688
    qApp->notify(m_mainWindow.data(), &goodDragEvent);
1689
    QCOMPARE(goodDragEvent.isAccepted(), true);
1690

1691
    QDropEvent goodDropEvent(QPoint(1, 1), Qt::LinkAction, &goodMimeData, Qt::LeftButton, Qt::NoModifier);
1692
    qApp->notify(m_mainWindow.data(), &goodDropEvent);
1693
    QCOMPARE(goodDropEvent.isAccepted(), true);
1694

1695
    QCOMPARE(m_tabWidget->count(), openedDatabasesCount + 1);
1696

1697
    MessageBox::setNextAnswer(MessageBox::No);
1698
    triggerAction("actionDatabaseClose");
1699

1700
    QTRY_COMPARE(m_tabWidget->count(), openedDatabasesCount);
1701
}
1702

1703
void TestGui::testSortGroups()
1704
{
1705
    auto* editGroupWidget = m_dbWidget->findChild<EditGroupWidget*>("editGroupWidget");
1706
    auto* nameEdit = editGroupWidget->findChild<QLineEdit*>("editName");
1707
    auto* editGroupWidgetButtonBox = editGroupWidget->findChild<QDialogButtonBox*>("buttonBox");
1708

1709
    // Create some sub-groups
1710
    Group* rootGroup = m_db->rootGroup();
1711
    Group* internetGroup = rootGroup->findGroupByPath("Internet");
1712
    m_dbWidget->groupView()->setCurrentGroup(internetGroup);
1713
    m_dbWidget->createGroup();
1714
    QTest::keyClicks(nameEdit, "Google");
1715
    QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
1716
    m_dbWidget->groupView()->setCurrentGroup(internetGroup);
1717
    m_dbWidget->createGroup();
1718
    QTest::keyClicks(nameEdit, "eBay");
1719
    QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
1720
    m_dbWidget->groupView()->setCurrentGroup(internetGroup);
1721
    m_dbWidget->createGroup();
1722
    QTest::keyClicks(nameEdit, "Amazon");
1723
    QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
1724
    m_dbWidget->groupView()->setCurrentGroup(internetGroup);
1725
    m_dbWidget->createGroup();
1726
    QTest::keyClicks(nameEdit, "Facebook");
1727
    QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
1728
    m_dbWidget->groupView()->setCurrentGroup(rootGroup);
1729

1730
    triggerAction("actionGroupSortAsc");
1731
    QList<Group*> children = rootGroup->children();
1732
    QCOMPARE(children[0]->name(), QString("eMail"));
1733
    QCOMPARE(children[1]->name(), QString("General"));
1734
    QCOMPARE(children[2]->name(), QString("Homebanking"));
1735
    QCOMPARE(children[3]->name(), QString("Internet"));
1736
    QCOMPARE(children[4]->name(), QString("Network"));
1737
    QCOMPARE(children[5]->name(), QString("Windows"));
1738
    QList<Group*> subChildren = internetGroup->children();
1739
    QCOMPARE(subChildren[0]->name(), QString("Amazon"));
1740
    QCOMPARE(subChildren[1]->name(), QString("eBay"));
1741
    QCOMPARE(subChildren[2]->name(), QString("Facebook"));
1742
    QCOMPARE(subChildren[3]->name(), QString("Google"));
1743

1744
    triggerAction("actionGroupSortDesc");
1745
    children = rootGroup->children();
1746
    QCOMPARE(children[0]->name(), QString("Windows"));
1747
    QCOMPARE(children[1]->name(), QString("Network"));
1748
    QCOMPARE(children[2]->name(), QString("Internet"));
1749
    QCOMPARE(children[3]->name(), QString("Homebanking"));
1750
    QCOMPARE(children[4]->name(), QString("General"));
1751
    QCOMPARE(children[5]->name(), QString("eMail"));
1752
    subChildren = internetGroup->children();
1753
    QCOMPARE(subChildren[0]->name(), QString("Google"));
1754
    QCOMPARE(subChildren[1]->name(), QString("Facebook"));
1755
    QCOMPARE(subChildren[2]->name(), QString("eBay"));
1756
    QCOMPARE(subChildren[3]->name(), QString("Amazon"));
1757

1758
    m_dbWidget->groupView()->setCurrentGroup(internetGroup);
1759
    triggerAction("actionGroupSortAsc");
1760
    children = rootGroup->children();
1761
    QCOMPARE(children[0]->name(), QString("Windows"));
1762
    QCOMPARE(children[1]->name(), QString("Network"));
1763
    QCOMPARE(children[2]->name(), QString("Internet"));
1764
    QCOMPARE(children[3]->name(), QString("Homebanking"));
1765
    QCOMPARE(children[4]->name(), QString("General"));
1766
    QCOMPARE(children[5]->name(), QString("eMail"));
1767
    subChildren = internetGroup->children();
1768
    QCOMPARE(subChildren[0]->name(), QString("Amazon"));
1769
    QCOMPARE(subChildren[1]->name(), QString("eBay"));
1770
    QCOMPARE(subChildren[2]->name(), QString("Facebook"));
1771
    QCOMPARE(subChildren[3]->name(), QString("Google"));
1772

1773
    m_dbWidget->groupView()->setCurrentGroup(rootGroup);
1774
    triggerAction("actionGroupSortAsc");
1775
    m_dbWidget->groupView()->setCurrentGroup(internetGroup);
1776
    triggerAction("actionGroupSortDesc");
1777
    children = rootGroup->children();
1778
    QCOMPARE(children[0]->name(), QString("eMail"));
1779
    QCOMPARE(children[1]->name(), QString("General"));
1780
    QCOMPARE(children[2]->name(), QString("Homebanking"));
1781
    QCOMPARE(children[3]->name(), QString("Internet"));
1782
    QCOMPARE(children[4]->name(), QString("Network"));
1783
    QCOMPARE(children[5]->name(), QString("Windows"));
1784
    subChildren = internetGroup->children();
1785
    QCOMPARE(subChildren[0]->name(), QString("Google"));
1786
    QCOMPARE(subChildren[1]->name(), QString("Facebook"));
1787
    QCOMPARE(subChildren[2]->name(), QString("eBay"));
1788
    QCOMPARE(subChildren[3]->name(), QString("Amazon"));
1789
}
1790

1791
void TestGui::testTrayRestoreHide()
1792
{
1793
    if (!QSystemTrayIcon::isSystemTrayAvailable()) {
1794
        QSKIP("QSystemTrayIcon::isSystemTrayAvailable() = false, skipping tray restore/hide test…");
1795
    }
1796

1797
#ifndef Q_OS_MACOS
1798
    m_mainWindow->hideWindow();
1799
    QVERIFY(!m_mainWindow->isVisible());
1800

1801
    auto* trayIcon = m_mainWindow->findChild<QSystemTrayIcon*>();
1802
    QVERIFY(trayIcon);
1803

1804
    trayIcon->activated(QSystemTrayIcon::Trigger);
1805
    QTRY_VERIFY(m_mainWindow->isVisible());
1806

1807
    trayIcon->activated(QSystemTrayIcon::Trigger);
1808
    QTRY_VERIFY(!m_mainWindow->isVisible());
1809

1810
    trayIcon->activated(QSystemTrayIcon::MiddleClick);
1811
    QTRY_VERIFY(m_mainWindow->isVisible());
1812

1813
    trayIcon->activated(QSystemTrayIcon::MiddleClick);
1814
    QTRY_VERIFY(!m_mainWindow->isVisible());
1815

1816
    trayIcon->activated(QSystemTrayIcon::DoubleClick);
1817
    QTRY_VERIFY(m_mainWindow->isVisible());
1818

1819
    trayIcon->activated(QSystemTrayIcon::DoubleClick);
1820
    QTRY_VERIFY(!m_mainWindow->isVisible());
1821

1822
    // Ensure window is visible at the end
1823
    trayIcon->activated(QSystemTrayIcon::DoubleClick);
1824
    QTRY_VERIFY(m_mainWindow->isVisible());
1825
#endif
1826
}
1827

1828
void TestGui::testShortcutConfig()
1829
{
1830
    // Action collection should not be empty
1831
    QVERIFY(!ActionCollection::instance()->actions().isEmpty());
1832

1833
    // Add an action, make sure it gets added
1834
    QAction* a = new QAction(ActionCollection::instance());
1835
    a->setObjectName("MyAction1");
1836
    ActionCollection::instance()->addAction(a);
1837
    QVERIFY(ActionCollection::instance()->actions().contains(a));
1838

1839
    const QKeySequence seq(Qt::CTRL + Qt::SHIFT + Qt::ALT + Qt::Key_N);
1840
    ActionCollection::instance()->setDefaultShortcut(a, seq);
1841
    QCOMPARE(ActionCollection::instance()->defaultShortcut(a), seq);
1842

1843
    bool v = false;
1844
    m_mainWindow->addAction(a);
1845
    connect(a, &QAction::triggered, ActionCollection::instance(), [&v] { v = !v; });
1846
    QTest::keyClick(m_mainWindow.data(), Qt::Key_N, Qt::ControlModifier | Qt::ShiftModifier | Qt::AltModifier);
1847
    QVERIFY(v);
1848

1849
    // Change shortcut and save
1850
    const QKeySequence newSeq(Qt::CTRL + Qt::SHIFT + Qt::ALT + Qt::Key_M);
1851
    a->setShortcut(newSeq);
1852
    QVERIFY(a->shortcut() != ActionCollection::instance()->defaultShortcut(a));
1853
    ActionCollection::instance()->saveShortcuts();
1854
    QCOMPARE(a->shortcut(), newSeq);
1855
    const auto shortcuts = Config::instance()->getShortcuts();
1856
    Config::ShortcutEntry entryForA;
1857
    for (const auto& s : shortcuts) {
1858
        if (s.name == a->objectName()) {
1859
            entryForA = s;
1860
            break;
1861
        }
1862
    }
1863
    QCOMPARE(entryForA.name, a->objectName());
1864
    QCOMPARE(QKeySequence::fromString(entryForA.shortcut), a->shortcut());
1865

1866
    // trigger the old shortcut
1867
    QTest::keyClick(m_mainWindow.data(), Qt::Key_N, Qt::ControlModifier | Qt::ShiftModifier | Qt::AltModifier);
1868
    QVERIFY(v); // value of v should not change
1869
    QTest::keyClick(m_mainWindow.data(), Qt::Key_M, Qt::ControlModifier | Qt::ShiftModifier | Qt::AltModifier);
1870
    QVERIFY(!v);
1871
    disconnect(a, nullptr, nullptr, nullptr);
1872
}
1873

1874
void TestGui::testAutoType()
1875
{
1876
    // Clear entries from root group to guarantee order
1877
    for (Entry* entry : m_db->rootGroup()->entries()) {
1878
        m_db->rootGroup()->removeEntry(entry);
1879
    }
1880
    Tools::wait(150);
1881

1882
    // 1. Create an entry with Auto-Type disabled
1883

1884
    // 1.a) Click the new entry button and set the title
1885
    auto* entryNewAction = m_mainWindow->findChild<QAction*>("actionEntryNew");
1886
    QVERIFY(entryNewAction->isEnabled());
1887

1888
    auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
1889
    QVERIFY(toolBar);
1890

1891
    QWidget* entryNewWidget = toolBar->widgetForAction(entryNewAction);
1892
    QVERIFY(entryNewWidget->isVisible());
1893
    QVERIFY(entryNewWidget->isEnabled());
1894

1895
    QTest::mouseClick(entryNewWidget, Qt::LeftButton);
1896
    QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
1897

1898
    auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
1899
    QVERIFY(editEntryWidget);
1900

1901
    auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
1902
    QVERIFY(titleEdit);
1903

1904
    QTest::keyClicks(titleEdit, "1. Entry With Disabled Auto-Type");
1905

1906
    auto* usernameComboBox = editEntryWidget->findChild<QComboBox*>("usernameComboBox");
1907
    QVERIFY(usernameComboBox);
1908

1909
    QTest::mouseClick(usernameComboBox, Qt::LeftButton);
1910
    QTest::keyClicks(usernameComboBox, "AutocompletionUsername");
1911

1912
    // 1.b) Uncheck Auto-Type checkbox
1913
    editEntryWidget->setCurrentPage(3);
1914
    auto* enableAutoTypeButton = editEntryWidget->findChild<QCheckBox*>("enableButton");
1915
    QVERIFY(enableAutoTypeButton);
1916
    QVERIFY(enableAutoTypeButton->isVisible());
1917
    QVERIFY(enableAutoTypeButton->isEnabled());
1918

1919
    enableAutoTypeButton->click();
1920
    QVERIFY(!enableAutoTypeButton->isChecked());
1921

1922
    // 1.c) Save changes
1923
    editEntryWidget->setCurrentPage(0);
1924
    auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
1925
    QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
1926

1927
    // 2. Create an entry with default/inherited Auto-Type sequence
1928

1929
    // 2.a) Click the new entry button and set the title
1930
    QTest::mouseClick(entryNewWidget, Qt::LeftButton);
1931
    QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
1932
    QTest::keyClicks(titleEdit, "2. Entry With Default Auto-Type Sequence");
1933
    QTest::mouseClick(usernameComboBox, Qt::LeftButton);
1934
    QTest::keyClicks(usernameComboBox, "AutocompletionUsername");
1935

1936
    // 2.b) Confirm AutoType is enabled and default
1937
    editEntryWidget->setCurrentPage(3);
1938
    QVERIFY(enableAutoTypeButton->isChecked());
1939
    auto* inheritSequenceButton = editEntryWidget->findChild<QRadioButton*>("inheritSequenceButton");
1940
    QVERIFY(inheritSequenceButton->isChecked());
1941

1942
    // 2.c) Save changes
1943
    editEntryWidget->setCurrentPage(0);
1944
    QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
1945

1946
    // 3. Create an entry with custom Auto-Type sequence
1947

1948
    // 3.a) Click the new entry button and set the title
1949
    QTest::mouseClick(entryNewWidget, Qt::LeftButton);
1950
    QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
1951
    QTest::keyClicks(titleEdit, "3. Entry With Custom Auto-Type Sequence");
1952
    QTest::mouseClick(usernameComboBox, Qt::LeftButton);
1953
    QTest::keyClicks(usernameComboBox, "AutocompletionUsername");
1954

1955
    // 3.b) Confirm AutoType is enabled and set custom sequence
1956
    editEntryWidget->setCurrentPage(3);
1957
    QVERIFY(enableAutoTypeButton->isChecked());
1958
    auto* customSequenceButton = editEntryWidget->findChild<QRadioButton*>("customSequenceButton");
1959
    QTest::mouseClick(customSequenceButton, Qt::LeftButton);
1960
    QVERIFY(customSequenceButton->isChecked());
1961
    QVERIFY(!inheritSequenceButton->isChecked());
1962
    auto* sequenceEdit = editEntryWidget->findChild<QLineEdit*>("sequenceEdit");
1963
    QVERIFY(sequenceEdit);
1964
    sequenceEdit->setFocus();
1965
    QTRY_VERIFY(sequenceEdit->hasFocus());
1966
    QTest::keyClicks(sequenceEdit, "{USERNAME}{TAB}{TAB}{PASSWORD}{ENTER}");
1967

1968
    // 3.c) Save changes
1969
    editEntryWidget->setCurrentPage(0);
1970
    QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
1971
    QApplication::processEvents();
1972

1973
    // Check total number of entries matches expected
1974
    auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
1975
    QVERIFY(entryView);
1976
    QTRY_COMPARE(entryView->model()->rowCount(), 3);
1977

1978
    // Sort entries by title
1979
    entryView->sortByColumn(1, Qt::AscendingOrder);
1980

1981
    // Select first entry
1982
    entryView->selectionModel()->clearSelection();
1983
    QModelIndex entryIndex = entryView->model()->index(0, 0);
1984
    entryView->selectionModel()->select(entryIndex, QItemSelectionModel::Rows | QItemSelectionModel::Select);
1985

1986
    auto* entryPreviewWidget = m_dbWidget->findChild<EntryPreviewWidget*>("previewWidget");
1987
    QVERIFY(entryPreviewWidget->isVisible());
1988

1989
    // Check that the Autotype tab in entry preview pane is disabled for entry with disabled Auto-Type
1990
    auto* entryAutotypeTab = entryPreviewWidget->findChild<QWidget*>("entryAutotypeTab");
1991
    QVERIFY(!entryAutotypeTab->isEnabled());
1992

1993
    // Check that Auto-Type is disabled in the actual entry model as well
1994
    Entry* entry = entryView->entryFromIndex(entryIndex);
1995
    QVERIFY(!entry->autoTypeEnabled());
1996

1997
    // Select second entry
1998
    entryView->selectionModel()->clearSelection();
1999
    entryIndex = entryView->model()->index(1, 0);
2000
    entryView->selectionModel()->select(entryIndex, QItemSelectionModel::Rows | QItemSelectionModel::Select);
2001
    QVERIFY(entryPreviewWidget->isVisible());
2002

2003
    // Check that the Autotype tab in entry preview pane is enabled for entry with default Auto-Type sequence;
2004
    QVERIFY(entryAutotypeTab->isEnabled());
2005

2006
    // Check that Auto-Type is enabled in the actual entry model as well
2007
    entry = entryView->entryFromIndex(entryIndex);
2008
    QVERIFY(entry->autoTypeEnabled());
2009

2010
    // Select third entry
2011
    entryView->selectionModel()->clearSelection();
2012
    entryIndex = entryView->model()->index(2, 0);
2013
    entryView->selectionModel()->select(entryIndex, QItemSelectionModel::Rows | QItemSelectionModel::Select);
2014
    QVERIFY(entryPreviewWidget->isVisible());
2015

2016
    // Check that the Autotype tab in entry preview pane is enabled for entry with custom Auto-Type sequence
2017
    QVERIFY(entryAutotypeTab->isEnabled());
2018

2019
    // Check that Auto-Type is enabled in the actual entry model as well
2020
    entry = entryView->entryFromIndex(entryIndex);
2021
    QVERIFY(entry->autoTypeEnabled());
2022

2023
    // De-select third entry
2024
    entryView->selectionModel()->clearSelection();
2025
}
2026

2027
void TestGui::addCannedEntries()
2028
{
2029
    // Find buttons
2030
    auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
2031
    QWidget* entryNewWidget = toolBar->widgetForAction(m_mainWindow->findChild<QAction*>("actionEntryNew"));
2032
    auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
2033
    auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
2034
    auto* passwordEdit =
2035
        editEntryWidget->findChild<PasswordWidget*>("passwordEdit")->findChild<QLineEdit*>("passwordEdit");
2036

2037
    // Add entry "test" and confirm added
2038
    QTest::mouseClick(entryNewWidget, Qt::LeftButton);
2039
    QTest::keyClicks(titleEdit, "test");
2040
    auto* editEntryWidgetTagsEdit = editEntryWidget->findChild<TagsEdit*>("tagsList");
2041
    editEntryWidgetTagsEdit->tags(QStringList() << "testTag");
2042
    auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
2043
    QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
2044

2045
    // Add entry "something 2"
2046
    QTest::mouseClick(entryNewWidget, Qt::LeftButton);
2047
    QTest::keyClicks(titleEdit, "something 2");
2048
    QTest::keyClicks(passwordEdit, "something 2");
2049
    QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
2050

2051
    // Add entry "something 3"
2052
    QTest::mouseClick(entryNewWidget, Qt::LeftButton);
2053
    QTest::keyClicks(titleEdit, "something 3");
2054
    QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
2055
}
2056

2057
void TestGui::checkDatabase(const QString& filePath, const QString& expectedDbName)
2058
{
2059
    auto key = QSharedPointer<CompositeKey>::create();
2060
    key->addKey(QSharedPointer<PasswordKey>::create("a"));
2061
    auto dbSaved = QSharedPointer<Database>::create();
2062
    QVERIFY(dbSaved->open(filePath, key, nullptr));
2063
    QCOMPARE(dbSaved->metadata()->name(), expectedDbName);
2064
}
2065

2066
void TestGui::checkDatabase(const QString& filePath)
2067
{
2068
    checkDatabase(filePath.isEmpty() ? m_dbFilePath : filePath, m_db->metadata()->name());
2069
}
2070

2071
void TestGui::checkSaveDatabase()
2072
{
2073
    // Attempt to save the database up to two times to overcome transient file errors
2074
    QTRY_VERIFY(m_db->isModified());
2075
    QTRY_VERIFY(m_tabWidget->tabText(m_tabWidget->currentIndex()).endsWith("*"));
2076
    int i = 0;
2077
    do {
2078
        triggerAction("actionDatabaseSave");
2079
        if (!m_db->isModified()) {
2080
            checkDatabase();
2081
            return;
2082
        }
2083
        QWARN("Failed to save database, trying again...");
2084
        Tools::wait(250);
2085
    } while (++i < 2);
2086

2087
    QFAIL("Could not save database.");
2088
}
2089

2090
void TestGui::checkStatusBarText(const QString& textFragment)
2091
{
2092
    QApplication::processEvents();
2093
    QVERIFY(m_statusBarLabel->isVisible());
2094
    QTRY_VERIFY2(m_statusBarLabel->text().startsWith(textFragment),
2095
                 qPrintable(QString("'%1' doesn't start with '%2'").arg(m_statusBarLabel->text(), textFragment)));
2096
}
2097

2098
void TestGui::triggerAction(const QString& name)
2099
{
2100
    auto* action = m_mainWindow->findChild<QAction*>(name);
2101
    QVERIFY(action);
2102
    QVERIFY(action->isEnabled());
2103
    action->trigger();
2104
    QApplication::processEvents();
2105
}
2106

2107
void TestGui::dragAndDropGroup(const QModelIndex& sourceIndex,
2108
                               const QModelIndex& targetIndex,
2109
                               int row,
2110
                               bool expectedResult,
2111
                               const QString& expectedParentName,
2112
                               int expectedPos)
2113
{
2114
    QVERIFY(sourceIndex.isValid());
2115
    QVERIFY(targetIndex.isValid());
2116

2117
    auto groupModel = qobject_cast<GroupModel*>(m_dbWidget->findChild<GroupView*>("groupView")->model());
2118

2119
    QMimeData mimeData;
2120
    QByteArray encoded;
2121
    QDataStream stream(&encoded, QIODevice::WriteOnly);
2122
    Group* group = groupModel->groupFromIndex(sourceIndex);
2123
    stream << group->database()->uuid() << group->uuid();
2124
    mimeData.setData("application/x-keepassx-group", encoded);
2125

2126
    QCOMPARE(groupModel->dropMimeData(&mimeData, Qt::MoveAction, row, 0, targetIndex), expectedResult);
2127
    QCOMPARE(group->parentGroup()->name(), expectedParentName);
2128
    QCOMPARE(group->parentGroup()->children().indexOf(group), expectedPos);
2129
}
2130

2131
void TestGui::clickIndex(const QModelIndex& index,
2132
                         QAbstractItemView* view,
2133
                         Qt::MouseButton button,
2134
                         Qt::KeyboardModifiers stateKey)
2135
{
2136
    view->scrollTo(index);
2137
    QTest::mouseClick(view->viewport(), button, stateKey, view->visualRect(index).center());
2138
}
2139

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

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

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

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