1 /*
2  *  Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
3  *  Copyright (C) 2017 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 "TestGlobal.h"
21 #include "gui/Application.h"
22 
23 #include <QAction>
24 #include <QApplication>
25 #include <QCheckBox>
26 #include <QClipboard>
27 #include <QComboBox>
28 #include <QDebug>
29 #include <QDialogButtonBox>
30 #include <QLabel>
31 #include <QLineEdit>
32 #include <QListWidgetItem>
33 #include <QMimeData>
34 #include <QPlainTextEdit>
35 #include <QPushButton>
36 #include <QRadioButton>
37 #include <QSignalSpy>
38 #include <QSpinBox>
39 #include <QTest>
40 #include <QTimer>
41 #include <QToolBar>
42 #include <QToolButton>
43 #include <QTreeWidgetItem>
44 
45 #include "config-keepassx-tests.h"
46 #include "core/Bootstrap.h"
47 #include "core/Config.h"
48 #include "core/Database.h"
49 #include "core/Entry.h"
50 #include "core/Group.h"
51 #include "core/Metadata.h"
52 #include "core/PasswordHealth.h"
53 #include "core/Tools.h"
54 #include "crypto/Crypto.h"
55 #include "crypto/kdf/AesKdf.h"
56 #include "format/KeePass2Reader.h"
57 #include "gui/ApplicationSettingsWidget.h"
58 #include "gui/CategoryListWidget.h"
59 #include "gui/CloneDialog.h"
60 #include "gui/DatabaseTabWidget.h"
61 #include "gui/DatabaseWidget.h"
62 #include "gui/EntryPreviewWidget.h"
63 #include "gui/FileDialog.h"
64 #include "gui/MessageBox.h"
65 #include "gui/PasswordEdit.h"
66 #include "gui/PasswordGeneratorWidget.h"
67 #include "gui/SearchWidget.h"
68 #include "gui/TotpDialog.h"
69 #include "gui/TotpSetupDialog.h"
70 #include "gui/databasekey/KeyComponentWidget.h"
71 #include "gui/databasekey/KeyFileEditWidget.h"
72 #include "gui/databasekey/PasswordEditWidget.h"
73 #include "gui/dbsettings/DatabaseSettingsDialog.h"
74 #include "gui/entry/EditEntryWidget.h"
75 #include "gui/entry/EntryView.h"
76 #include "gui/group/EditGroupWidget.h"
77 #include "gui/group/GroupModel.h"
78 #include "gui/group/GroupView.h"
79 #include "gui/wizard/NewDatabaseWizard.h"
80 #include "keys/FileKey.h"
81 #include "keys/PasswordKey.h"
82 
83 #define TEST_MODAL_NO_WAIT(TEST_CODE)                                                                                  \
84     bool dialogFinished = false;                                                                                       \
85     QTimer::singleShot(0, [&]() { TEST_CODE dialogFinished = true; })
86 
87 #define TEST_MODAL(TEST_CODE)                                                                                          \
88     TEST_MODAL_NO_WAIT(TEST_CODE);                                                                                     \
89     QTRY_VERIFY(dialogFinished)
90 
main(int argc,char * argv[])91 int main(int argc, char* argv[])
92 {
93 #if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
94     QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
95     QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
96 #endif
97     Application app(argc, argv);
98     app.setApplicationName("KeePassXC");
99     app.setApplicationVersion(KEEPASSXC_VERSION);
100     app.setQuitOnLastWindowClosed(false);
101     app.setAttribute(Qt::AA_Use96Dpi, true);
102     app.applyTheme();
103     QTEST_DISABLE_KEYPAD_NAVIGATION
104     TestGui tc;
105     QTEST_SET_MAIN_SOURCE_PATH
106     return QTest::qExec(&tc, argc, argv);
107 }
108 
109 static QString dbFileName = QStringLiteral(KEEPASSX_TEST_DATA_DIR).append("/NewDatabase.kdbx");
110 
initTestCase()111 void TestGui::initTestCase()
112 {
113     QVERIFY(Crypto::init());
114     Config::createTempFileInstance();
115     // Disable autosave so we can test the modified file indicator
116     config()->set(Config::AutoSaveAfterEveryChange, false);
117     config()->set(Config::AutoSaveOnExit, false);
118     // Enable the tray icon so we can test hiding/restoring the windowQByteArray
119     config()->set(Config::GUI_ShowTrayIcon, true);
120     // Disable advanced settings mode (activate within individual tests to test advanced settings)
121     config()->set(Config::GUI_AdvancedSettings, false);
122     // Disable the update check first time alert
123     config()->set(Config::UpdateCheckMessageShown, true);
124 
125     Bootstrap::bootstrapApplication();
126 
127     m_mainWindow.reset(new MainWindow());
128     m_tabWidget = m_mainWindow->findChild<DatabaseTabWidget*>("tabWidget");
129     m_mainWindow->show();
130     m_mainWindow->resize(1024, 768);
131 }
132 
133 // Every test starts with opening the temp database
init()134 void TestGui::init()
135 {
136     // Copy the test database file to the temporary file
137     QVERIFY(m_dbFile.copyFromFile(dbFileName));
138 
139     m_dbFileName = QFileInfo(m_dbFile.fileName()).fileName();
140     m_dbFilePath = m_dbFile.fileName();
141 
142     // make sure window is activated or focus tests may fail
143     m_mainWindow->activateWindow();
144     QApplication::processEvents();
145 
146     fileDialog()->setNextFileName(m_dbFilePath);
147     triggerAction("actionDatabaseOpen");
148 
149     QApplication::processEvents();
150 
151     m_dbWidget = m_tabWidget->currentDatabaseWidget();
152     auto* databaseOpenWidget = m_tabWidget->currentDatabaseWidget()->findChild<QWidget*>("databaseOpenWidget");
153     QVERIFY(databaseOpenWidget);
154     auto* editPassword = databaseOpenWidget->findChild<QLineEdit*>("editPassword");
155     QVERIFY(editPassword);
156     editPassword->setFocus();
157 
158     QTest::keyClicks(editPassword, "a");
159     QTest::keyClick(editPassword, Qt::Key_Enter);
160 
161     QTRY_VERIFY(!m_dbWidget->isLocked());
162     m_db = m_dbWidget->database();
163 
164     QApplication::processEvents();
165 }
166 
167 // Every test ends with closing the temp database without saving
cleanup()168 void TestGui::cleanup()
169 {
170     // DO NOT save the database
171     m_db->markAsClean();
172     MessageBox::setNextAnswer(MessageBox::No);
173     triggerAction("actionDatabaseClose");
174     QApplication::processEvents();
175     MessageBox::setNextAnswer(MessageBox::NoButton);
176 
177     if (m_dbWidget) {
178         delete m_dbWidget;
179     }
180 }
181 
cleanupTestCase()182 void TestGui::cleanupTestCase()
183 {
184     m_dbFile.remove();
185 }
186 
testSettingsDefaultTabOrder()187 void TestGui::testSettingsDefaultTabOrder()
188 {
189     // check application settings default tab order
190     triggerAction("actionSettings");
191     auto* settingsWidget = m_mainWindow->findChild<ApplicationSettingsWidget*>();
192     QVERIFY(settingsWidget->isVisible());
193     QCOMPARE(settingsWidget->findChild<CategoryListWidget*>("categoryList")->currentCategory(), 0);
194     for (auto* w : settingsWidget->findChildren<QTabWidget*>()) {
195         if (w->currentIndex() != 0) {
196             QFAIL("Application settings contain QTabWidgets whose default index is not 0");
197         }
198     }
199     QTest::keyClick(settingsWidget, Qt::Key::Key_Escape);
200 
201     // check database settings default tab order
202     triggerAction("actionDatabaseSettings");
203     auto* dbSettingsWidget = m_mainWindow->findChild<DatabaseSettingsDialog*>();
204     QVERIFY(dbSettingsWidget->isVisible());
205     QCOMPARE(dbSettingsWidget->findChild<CategoryListWidget*>("categoryList")->currentCategory(), 0);
206     for (auto* w : dbSettingsWidget->findChildren<QTabWidget*>()) {
207         if (w->currentIndex() != 0) {
208             QFAIL("Database settings contain QTabWidgets whose default index is not 0");
209         }
210     }
211     QTest::keyClick(dbSettingsWidget, Qt::Key::Key_Escape);
212 }
213 
testCreateDatabase()214 void TestGui::testCreateDatabase()
215 {
216     TEST_MODAL_NO_WAIT(
217         NewDatabaseWizard * wizard; QTRY_VERIFY(wizard = m_tabWidget->findChild<NewDatabaseWizard*>());
218 
219         QTest::keyClicks(wizard->currentPage()->findChild<QLineEdit*>("databaseName"), "Test Name");
220         QTest::keyClicks(wizard->currentPage()->findChild<QLineEdit*>("databaseDescription"), "Test Description");
221         QCOMPARE(wizard->currentId(), 0);
222 
223         QTest::keyClick(wizard, Qt::Key_Enter);
224         QCOMPARE(wizard->currentId(), 1);
225 
226         auto decryptionTimeSlider = wizard->currentPage()->findChild<QSlider*>("decryptionTimeSlider");
227         auto algorithmComboBox = wizard->currentPage()->findChild<QComboBox*>("algorithmComboBox");
228         QTRY_VERIFY(decryptionTimeSlider->isVisible());
229         QVERIFY(!algorithmComboBox->isVisible());
230         auto advancedToggle = wizard->currentPage()->findChild<QPushButton*>("advancedSettingsButton");
231         QTest::mouseClick(advancedToggle, Qt::MouseButton::LeftButton);
232         QTRY_VERIFY(!decryptionTimeSlider->isVisible());
233         QVERIFY(algorithmComboBox->isVisible());
234 
235         auto rounds = wizard->currentPage()->findChild<QSpinBox*>("transformRoundsSpinBox");
236         QVERIFY(rounds);
237         QVERIFY(rounds->isVisible());
238         QTest::mouseClick(rounds, Qt::MouseButton::LeftButton);
239         QTest::keyClick(rounds, Qt::Key_A, Qt::ControlModifier);
240         QTest::keyClicks(rounds, "2");
241         QTest::keyClick(rounds, Qt::Key_Tab);
242         QTest::keyClick(rounds, Qt::Key_Tab);
243 
244         auto memory = wizard->currentPage()->findChild<QSpinBox*>("memorySpinBox");
245         QVERIFY(memory);
246         QVERIFY(memory->isVisible());
247         QTest::mouseClick(memory, Qt::MouseButton::LeftButton);
248         QTest::keyClick(memory, Qt::Key_A, Qt::ControlModifier);
249         QTest::keyClicks(memory, "50");
250         QTest::keyClick(memory, Qt::Key_Tab);
251 
252         auto parallelism = wizard->currentPage()->findChild<QSpinBox*>("parallelismSpinBox");
253         QVERIFY(parallelism);
254         QVERIFY(parallelism->isVisible());
255         QTest::mouseClick(parallelism, Qt::MouseButton::LeftButton);
256         QTest::keyClick(parallelism, Qt::Key_A, Qt::ControlModifier);
257         QTest::keyClicks(parallelism, "1");
258         QTest::keyClick(parallelism, Qt::Key_Enter);
259 
260         QCOMPARE(wizard->currentId(), 2);
261 
262         // enter password
263         auto* passwordWidget = wizard->currentPage()->findChild<PasswordEditWidget*>();
264         QCOMPARE(passwordWidget->visiblePage(), KeyFileEditWidget::Page::Edit);
265         auto* passwordEdit = passwordWidget->findChild<QLineEdit*>("enterPasswordEdit");
266         auto* passwordRepeatEdit = passwordWidget->findChild<QLineEdit*>("repeatPasswordEdit");
267         QTRY_VERIFY(passwordEdit->isVisible());
268         QTRY_VERIFY(passwordEdit->hasFocus());
269         QTest::keyClicks(passwordEdit, "test");
270         QTest::keyClick(passwordEdit, Qt::Key::Key_Tab);
271         QTest::keyClicks(passwordRepeatEdit, "test");
272 
273         // add key file
274         auto* additionalOptionsButton = wizard->currentPage()->findChild<QPushButton*>("additionalKeyOptionsToggle");
275         auto* keyFileWidget = wizard->currentPage()->findChild<KeyFileEditWidget*>();
276         QVERIFY(additionalOptionsButton->isVisible());
277         QTest::mouseClick(additionalOptionsButton, Qt::MouseButton::LeftButton);
278         QTRY_VERIFY(keyFileWidget->isVisible());
279         QTRY_VERIFY(!additionalOptionsButton->isVisible());
280         QCOMPARE(passwordWidget->visiblePage(), KeyFileEditWidget::Page::Edit);
281         QTest::mouseClick(keyFileWidget->findChild<QPushButton*>("addButton"), Qt::MouseButton::LeftButton);
282         auto* fileEdit = keyFileWidget->findChild<QLineEdit*>("keyFileLineEdit");
283         QTRY_VERIFY(fileEdit);
284         QTRY_VERIFY(fileEdit->isVisible());
285         fileDialog()->setNextFileName(QString("%1/%2").arg(QString(KEEPASSX_TEST_DATA_DIR), "FileKeyHashed.key"));
286         QTest::keyClick(keyFileWidget->findChild<QPushButton*>("addButton"), Qt::Key::Key_Enter);
287         QVERIFY(fileEdit->hasFocus());
288         auto* browseButton = keyFileWidget->findChild<QPushButton*>("browseKeyFileButton");
289         QTest::keyClick(browseButton, Qt::Key::Key_Enter);
290         QCOMPARE(fileEdit->text(), QString("%1/%2").arg(QString(KEEPASSX_TEST_DATA_DIR), "FileKeyHashed.key"));
291 
292         // save database to temporary file
293         TemporaryFile tmpFile;
294         QVERIFY(tmpFile.open());
295         tmpFile.close();
296         fileDialog()->setNextFileName(tmpFile.fileName());
297 
298         QTest::keyClick(fileEdit, Qt::Key::Key_Enter);
299         tmpFile.remove(););
300 
301     triggerAction("actionDatabaseNew");
302 
303     // there is a new empty db
304     m_db = m_tabWidget->currentDatabaseWidget()->database();
305     QCOMPARE(m_db->rootGroup()->children().size(), 0);
306 
307     // check meta data
308     QCOMPARE(m_db->metadata()->name(), QString("Test Name"));
309     QCOMPARE(m_db->metadata()->description(), QString("Test Description"));
310 
311     // check key and encryption
312     QCOMPARE(m_db->key()->keys().size(), 2);
313     QCOMPARE(m_db->kdf()->rounds(), 2);
314     QCOMPARE(m_db->kdf()->uuid(), KeePass2::KDF_ARGON2D);
315     QCOMPARE(m_db->cipher(), KeePass2::CIPHER_AES256);
316     auto compositeKey = QSharedPointer<CompositeKey>::create();
317     compositeKey->addKey(QSharedPointer<PasswordKey>::create("test"));
318     auto fileKey = QSharedPointer<FileKey>::create();
319     fileKey->load(QString("%1/%2").arg(QString(KEEPASSX_TEST_DATA_DIR), "FileKeyHashed.key"));
320     compositeKey->addKey(fileKey);
321     QCOMPARE(m_db->key()->rawKey(), compositeKey->rawKey());
322 
323     // close the new database
324     MessageBox::setNextAnswer(MessageBox::No);
325     triggerAction("actionDatabaseClose");
326 
327     // Wait for dialog to terminate
328     QTRY_VERIFY(dialogFinished);
329 }
330 
testMergeDatabase()331 void TestGui::testMergeDatabase()
332 {
333     // It is safe to ignore the warning this line produces
334     QSignalSpy dbMergeSpy(m_dbWidget.data(), SIGNAL(databaseMerged(QSharedPointer<Database>)));
335     QApplication::processEvents();
336 
337     // set file to merge from
338     fileDialog()->setNextFileName(QString(KEEPASSX_TEST_DATA_DIR).append("/MergeDatabase.kdbx"));
339     triggerAction("actionDatabaseMerge");
340 
341     QTRY_COMPARE(QApplication::focusWidget()->objectName(), QString("editPassword"));
342     auto* editPasswordMerge = QApplication::focusWidget();
343     QVERIFY(editPasswordMerge->isVisible());
344 
345     QTest::keyClicks(editPasswordMerge, "a");
346     QTest::keyClick(editPasswordMerge, Qt::Key_Enter);
347 
348     QTRY_COMPARE(dbMergeSpy.count(), 1);
349     QTRY_VERIFY(m_tabWidget->tabName(m_tabWidget->currentIndex()).contains("*"));
350 
351     m_db = m_tabWidget->currentDatabaseWidget()->database();
352 
353     // there are seven child groups of the root group
354     QCOMPARE(m_db->rootGroup()->children().size(), 7);
355     // the merged group should contain an entry
356     QCOMPARE(m_db->rootGroup()->children().at(6)->entries().size(), 1);
357     // the General group contains one entry merged from the other db
358     QCOMPARE(m_db->rootGroup()->findChildByName("General")->entries().size(), 1);
359 }
360 
testAutoreloadDatabase()361 void TestGui::testAutoreloadDatabase()
362 {
363     config()->set(Config::AutoReloadOnChange, false);
364 
365     // Test accepting new file in autoreload
366     MessageBox::setNextAnswer(MessageBox::Yes);
367     // Overwrite the current database with the temp data
368     QVERIFY(m_dbFile.copyFromFile(QString(KEEPASSX_TEST_DATA_DIR).append("/MergeDatabase.kdbx")));
369 
370     QTRY_VERIFY(m_db != m_dbWidget->database());
371     m_db = m_dbWidget->database();
372 
373     // the General group contains one entry from the new db data
374     QCOMPARE(m_db->rootGroup()->findChildByName("General")->entries().size(), 1);
375     QVERIFY(!m_tabWidget->tabName(m_tabWidget->currentIndex()).endsWith("*"));
376 
377     // Reset the state
378     cleanup();
379     init();
380 
381     // Test rejecting new file in autoreload
382     MessageBox::setNextAnswer(MessageBox::No);
383     // Overwrite the current database with the temp data
384     QVERIFY(m_dbFile.copyFromFile(QString(KEEPASSX_TEST_DATA_DIR).append("/MergeDatabase.kdbx")));
385 
386     // Ensure the merge did not take place
387     QCOMPARE(m_db->rootGroup()->findChildByName("General")->entries().size(), 0);
388     QTRY_VERIFY(m_tabWidget->tabName(m_tabWidget->currentIndex()).endsWith("*"));
389 
390     // Reset the state
391     cleanup();
392     init();
393 
394     // Test accepting a merge of edits into autoreload
395     // Turn on autoload so we only get one messagebox (for the merge)
396     config()->set(Config::AutoReloadOnChange, true);
397     // Modify some entries
398     testEditEntry();
399 
400     // This is saying yes to merging the entries
401     MessageBox::setNextAnswer(MessageBox::Merge);
402     // Overwrite the current database with the temp data
403     QVERIFY(m_dbFile.copyFromFile(QString(KEEPASSX_TEST_DATA_DIR).append("/MergeDatabase.kdbx")));
404 
405     QTRY_VERIFY(m_db != m_dbWidget->database());
406     m_db = m_dbWidget->database();
407 
408     QCOMPARE(m_db->rootGroup()->findChildByName("General")->entries().size(), 1);
409     QTRY_VERIFY(m_tabWidget->tabText(m_tabWidget->currentIndex()).endsWith("*"));
410 }
411 
testTabs()412 void TestGui::testTabs()
413 {
414     QCOMPARE(m_tabWidget->count(), 1);
415     QCOMPARE(m_tabWidget->tabName(m_tabWidget->currentIndex()), m_dbFileName);
416 }
417 
testEditEntry()418 void TestGui::testEditEntry()
419 {
420     auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
421     auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
422 
423     entryView->setFocus();
424     QVERIFY(entryView->hasFocus());
425 
426     // Select the first entry in the database
427     QModelIndex entryItem = entryView->model()->index(0, 1);
428     Entry* entry = entryView->entryFromIndex(entryItem);
429     clickIndex(entryItem, entryView, Qt::LeftButton);
430 
431     // Confirm the edit action button is enabled
432     auto* entryEditAction = m_mainWindow->findChild<QAction*>("actionEntryEdit");
433     QVERIFY(entryEditAction->isEnabled());
434     QWidget* entryEditWidget = toolBar->widgetForAction(entryEditAction);
435     QVERIFY(entryEditWidget->isVisible());
436     QVERIFY(entryEditWidget->isEnabled());
437 
438     // Record current history count
439     int editCount = entry->historyItems().size();
440 
441     // Edit the first entry ("Sample Entry")
442     QTest::mouseClick(entryEditWidget, Qt::LeftButton);
443     QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
444     auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
445     auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
446     QTest::keyClicks(titleEdit, "_test");
447 
448     auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
449     QVERIFY(editEntryWidgetButtonBox);
450     auto* okButton = editEntryWidgetButtonBox->button(QDialogButtonBox::Ok);
451     QVERIFY(okButton);
452     auto* applyButton = editEntryWidgetButtonBox->button(QDialogButtonBox::Apply);
453     QVERIFY(applyButton);
454 
455     // Apply the edit
456     QTRY_VERIFY(applyButton->isEnabled());
457     QTest::mouseClick(applyButton, Qt::LeftButton);
458     QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
459     QCOMPARE(entry->title(), QString("Sample Entry_test"));
460     QCOMPARE(entry->historyItems().size(), ++editCount);
461     QVERIFY(!applyButton->isEnabled());
462 
463     // Test the "known bad" checkbox
464     editEntryWidget->setCurrentPage(1);
465     auto knownBadCheckBox = editEntryWidget->findChild<QCheckBox*>("knownBadCheckBox");
466     QVERIFY(knownBadCheckBox);
467     QCOMPARE(knownBadCheckBox->isChecked(), false);
468     knownBadCheckBox->setChecked(true);
469     QTest::mouseClick(applyButton, Qt::LeftButton);
470     QCOMPARE(entry->historyItems().size(), ++editCount);
471     QCOMPARE(entry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD), true);
472     QCOMPARE(entry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD), TRUE_STR);
473 
474     // Test entry colors (simulate choosing a color)
475     editEntryWidget->setCurrentPage(1);
476     auto fgColor = QString("#FF0000");
477     auto bgColor = QString("#0000FF");
478     // Set foreground color
479     auto colorButton = editEntryWidget->findChild<QPushButton*>("fgColorButton");
480     auto colorCheckBox = editEntryWidget->findChild<QCheckBox*>("fgColorCheckBox");
481     colorButton->setProperty("color", fgColor);
482     colorCheckBox->setChecked(true);
483     // Set background color
484     colorButton = editEntryWidget->findChild<QPushButton*>("bgColorButton");
485     colorCheckBox = editEntryWidget->findChild<QCheckBox*>("bgColorCheckBox");
486     colorButton->setProperty("color", bgColor);
487     colorCheckBox->setChecked(true);
488     QTest::mouseClick(applyButton, Qt::LeftButton);
489     QCOMPARE(entry->historyItems().size(), ++editCount);
490 
491     // Test protected attributes
492     editEntryWidget->setCurrentPage(1);
493     auto* attrTextEdit = editEntryWidget->findChild<QPlainTextEdit*>("attributesEdit");
494     QTest::mouseClick(editEntryWidget->findChild<QAbstractButton*>("addAttributeButton"), Qt::LeftButton);
495     QString attrText = "TEST TEXT";
496     QTest::keyClicks(attrTextEdit, attrText);
497     QCOMPARE(attrTextEdit->toPlainText(), attrText);
498     QTest::mouseClick(editEntryWidget->findChild<QAbstractButton*>("protectAttributeButton"), Qt::LeftButton);
499     QVERIFY(attrTextEdit->toPlainText().contains("PROTECTED"));
500     QTest::mouseClick(editEntryWidget->findChild<QAbstractButton*>("revealAttributeButton"), Qt::LeftButton);
501     QCOMPARE(attrTextEdit->toPlainText(), attrText);
502     editEntryWidget->setCurrentPage(0);
503 
504     // Save the edit (press OK)
505     QTest::mouseClick(okButton, Qt::LeftButton);
506     QApplication::processEvents();
507 
508     // Confirm edit was made
509     QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
510     QCOMPARE(entry->title(), QString("Sample Entry_test"));
511     QCOMPARE(entry->foregroundColor().toUpper(), fgColor.toUpper());
512     QCOMPARE(entryItem.data(Qt::ForegroundRole), QVariant(fgColor));
513     QCOMPARE(entry->backgroundColor().toUpper(), bgColor.toUpper());
514     QCOMPARE(entryItem.data(Qt::BackgroundRole), QVariant(bgColor));
515     QCOMPARE(entry->historyItems().size(), ++editCount);
516 
517     // Confirm modified indicator is showing
518     QTRY_COMPARE(m_tabWidget->tabName(m_tabWidget->currentIndex()), QString("%1*").arg(m_dbFileName));
519 
520     // Test copy & paste newline sanitization
521     QTest::mouseClick(entryEditWidget, Qt::LeftButton);
522     okButton = editEntryWidgetButtonBox->button(QDialogButtonBox::Ok);
523     QVERIFY(okButton);
524     QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
525     titleEdit->setText("multiline\ntitle");
526     editEntryWidget->findChild<QComboBox*>("usernameComboBox")->lineEdit()->setText("multiline\nusername");
527     editEntryWidget->findChild<QLineEdit*>("passwordEdit")->setText("multiline\npassword");
528     editEntryWidget->findChild<QLineEdit*>("urlEdit")->setText("multiline\nurl");
529     QTest::mouseClick(okButton, Qt::LeftButton);
530 
531     QCOMPARE(entry->title(), QString("multiline title"));
532     QCOMPARE(entry->username(), QString("multiline username"));
533     // here we keep newlines, so users can't lock themselves out accidentally
534     QCOMPARE(entry->password(), QString("multiline\npassword"));
535     QCOMPARE(entry->url(), QString("multiline url"));
536 }
537 
testSearchEditEntry()538 void TestGui::testSearchEditEntry()
539 {
540     // Regression test for Issue #1447 -- Uses example from issue description
541 
542     // Find buttons for group creation
543     auto* editGroupWidget = m_dbWidget->findChild<EditGroupWidget*>("editGroupWidget");
544     auto* nameEdit = editGroupWidget->findChild<QLineEdit*>("editName");
545     auto* editGroupWidgetButtonBox = editGroupWidget->findChild<QDialogButtonBox*>("buttonBox");
546 
547     // Add groups "Good" and "Bad"
548     m_dbWidget->createGroup();
549     QTest::keyClicks(nameEdit, "Good");
550     QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
551     m_dbWidget->groupView()->setCurrentGroup(m_db->rootGroup()); // Makes "Good" and "Bad" on the same level
552     m_dbWidget->createGroup();
553     QTest::keyClicks(nameEdit, "Bad");
554     QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
555     m_dbWidget->groupView()->setCurrentGroup(m_db->rootGroup());
556 
557     // Find buttons for entry creation
558     auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
559     QWidget* entryNewWidget = toolBar->widgetForAction(m_mainWindow->findChild<QAction*>("actionEntryNew"));
560     auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
561     auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
562     auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
563 
564     // Create "Doggy" in "Good"
565     Group* goodGroup = m_dbWidget->currentGroup()->findChildByName(QString("Good"));
566     m_dbWidget->groupView()->setCurrentGroup(goodGroup);
567     QTest::mouseClick(entryNewWidget, Qt::LeftButton);
568     QTest::keyClicks(titleEdit, "Doggy");
569     QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
570     // Select "Bad" group in groupView
571     Group* badGroup = m_db->rootGroup()->findChildByName(QString("Bad"));
572     m_dbWidget->groupView()->setCurrentGroup(badGroup);
573 
574     // Search for "Doggy" entry
575     auto* searchWidget = toolBar->findChild<SearchWidget*>("SearchWidget");
576     auto* searchTextEdit = searchWidget->findChild<QLineEdit*>("searchEdit");
577     QTest::mouseClick(searchTextEdit, Qt::LeftButton);
578     QTest::keyClicks(searchTextEdit, "Doggy");
579     QTRY_VERIFY(m_dbWidget->isSearchActive());
580 
581     // Goto "Doggy"'s edit view
582     QTest::keyClick(searchTextEdit, Qt::Key_Return);
583     QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
584 
585     // Check the path in header is "parent-group > entry"
586     QCOMPARE(m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget")->findChild<QLabel*>("headerLabel")->text(),
587              QStringLiteral("Good \u2022 Doggy \u2022 Edit entry"));
588 }
589 
testAddEntry()590 void TestGui::testAddEntry()
591 {
592     auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
593     auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
594 
595     // Find the new entry action
596     auto* entryNewAction = m_mainWindow->findChild<QAction*>("actionEntryNew");
597     QVERIFY(entryNewAction->isEnabled());
598 
599     // Find the button associated with the new entry action
600     QWidget* entryNewWidget = toolBar->widgetForAction(entryNewAction);
601     QVERIFY(entryNewWidget->isVisible());
602     QVERIFY(entryNewWidget->isEnabled());
603 
604     // Click the new entry button and check that we enter edit mode
605     QTest::mouseClick(entryNewWidget, Qt::LeftButton);
606     QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
607 
608     // Add entry "test" and confirm added
609     auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
610     auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
611     QTest::keyClicks(titleEdit, "test");
612     auto* usernameComboBox = editEntryWidget->findChild<QComboBox*>("usernameComboBox");
613     QVERIFY(usernameComboBox);
614     QTest::mouseClick(usernameComboBox, Qt::LeftButton);
615     QTest::keyClicks(usernameComboBox, "AutocompletionUsername");
616     auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
617     QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
618 
619     QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
620     QModelIndex item = entryView->model()->index(1, 1);
621     Entry* entry = entryView->entryFromIndex(item);
622 
623     QCOMPARE(entry->title(), QString("test"));
624     QCOMPARE(entry->username(), QString("AutocompletionUsername"));
625     QCOMPARE(entry->historyItems().size(), 0);
626 
627     m_db->updateCommonUsernames();
628 
629     // Add entry "something 2"
630     QTest::mouseClick(entryNewWidget, Qt::LeftButton);
631     QTest::keyClicks(titleEdit, "something 2");
632     QTest::mouseClick(usernameComboBox, Qt::LeftButton);
633     QTest::keyClicks(usernameComboBox, "Auto");
634     QTest::keyPress(usernameComboBox, Qt::Key_Right);
635     auto* passwordEdit = editEntryWidget->findChild<QLineEdit*>("passwordEdit");
636     QTest::keyClicks(passwordEdit, "something 2");
637     QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
638 
639     QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
640     item = entryView->model()->index(1, 1);
641     entry = entryView->entryFromIndex(item);
642 
643     QCOMPARE(entry->title(), QString("something 2"));
644     QCOMPARE(entry->username(), QString("AutocompletionUsername"));
645     QCOMPARE(entry->historyItems().size(), 0);
646 
647     // Add entry "something 5" but click cancel button (does NOT add entry)
648     QTest::mouseClick(entryNewWidget, Qt::LeftButton);
649     QTest::keyClicks(titleEdit, "something 5");
650     MessageBox::setNextAnswer(MessageBox::Discard);
651     QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Cancel), Qt::LeftButton);
652 
653     QApplication::processEvents();
654 
655     // Confirm entry count
656     QTRY_COMPARE(entryView->model()->rowCount(), 3);
657 }
658 
testPasswordEntryEntropy()659 void TestGui::testPasswordEntryEntropy()
660 {
661     auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
662 
663     // Find the new entry action
664     auto* entryNewAction = m_mainWindow->findChild<QAction*>("actionEntryNew");
665     QVERIFY(entryNewAction->isEnabled());
666 
667     // Find the button associated with the new entry action
668     QWidget* entryNewWidget = toolBar->widgetForAction(entryNewAction);
669     QVERIFY(entryNewWidget->isVisible());
670     QVERIFY(entryNewWidget->isEnabled());
671 
672     // Click the new entry button and check that we enter edit mode
673     QTest::mouseClick(entryNewWidget, Qt::LeftButton);
674     QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
675 
676     // Add entry "test" and confirm added
677     auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
678     auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
679     QTest::keyClicks(titleEdit, "test");
680 
681     // Open the password generator
682     auto* passwordEdit = editEntryWidget->findChild<PasswordEdit*>();
683     QVERIFY(passwordEdit);
684     QTest::mouseClick(passwordEdit, Qt::LeftButton);
685 
686     QTest::keyClick(passwordEdit, Qt::Key_G, Qt::ControlModifier);
687 
688     TEST_MODAL(PasswordGeneratorWidget * pwGeneratorWidget;
689                QTRY_VERIFY(pwGeneratorWidget = m_dbWidget->findChild<PasswordGeneratorWidget*>());
690 
691                // Type in some password
692                auto* generatedPassword = pwGeneratorWidget->findChild<QLineEdit*>("editNewPassword");
693                auto* entropyLabel = pwGeneratorWidget->findChild<QLabel*>("entropyLabel");
694                auto* strengthLabel = pwGeneratorWidget->findChild<QLabel*>("strengthLabel");
695 
696                generatedPassword->setText("");
697                QTest::keyClicks(generatedPassword, "hello");
698                QCOMPARE(entropyLabel->text(), QString("Entropy: 6.38 bit"));
699                QCOMPARE(strengthLabel->text(), QString("Password Quality: Poor"));
700 
701                generatedPassword->setText("");
702                QTest::keyClicks(generatedPassword, "helloworld");
703                QCOMPARE(entropyLabel->text(), QString("Entropy: 13.10 bit"));
704                QCOMPARE(strengthLabel->text(), QString("Password Quality: Poor"));
705 
706                generatedPassword->setText("");
707                QTest::keyClicks(generatedPassword, "password1");
708                QCOMPARE(entropyLabel->text(), QString("Entropy: 4.00 bit"));
709                QCOMPARE(strengthLabel->text(), QString("Password Quality: Poor"));
710 
711                generatedPassword->setText("");
712                QTest::keyClicks(generatedPassword, "D0g..................");
713                QCOMPARE(entropyLabel->text(), QString("Entropy: 19.02 bit"));
714                QCOMPARE(strengthLabel->text(), QString("Password Quality: Poor"));
715 
716                generatedPassword->setText("");
717                QTest::keyClicks(generatedPassword, "Tr0ub4dour&3");
718                QCOMPARE(entropyLabel->text(), QString("Entropy: 30.87 bit"));
719                QCOMPARE(strengthLabel->text(), QString("Password Quality: Poor"));
720 
721                generatedPassword->setText("");
722                QTest::keyClicks(generatedPassword, "correcthorsebatterystaple");
723                QCOMPARE(entropyLabel->text(), QString("Entropy: 47.98 bit"));
724                QCOMPARE(strengthLabel->text(), QString("Password Quality: Weak"));
725 
726                generatedPassword->setText("");
727                QTest::keyClicks(generatedPassword, "YQC3kbXbjC652dTDH");
728                QCOMPARE(entropyLabel->text(), QString("Entropy: 95.83 bit"));
729                QCOMPARE(strengthLabel->text(), QString("Password Quality: Good"));
730 
731                generatedPassword->setText("");
732                QTest::keyClicks(generatedPassword, "Bs5ZFfthWzR8DGFEjaCM6bGqhmCT4km");
733                QCOMPARE(entropyLabel->text(), QString("Entropy: 174.59 bit"));
734                QCOMPARE(strengthLabel->text(), QString("Password Quality: Excellent"));
735 
736                QTest::mouseClick(generatedPassword, Qt::LeftButton);
737                QTest::keyClick(generatedPassword, Qt::Key_Escape););
738 }
739 
testDicewareEntryEntropy()740 void TestGui::testDicewareEntryEntropy()
741 {
742     auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
743 
744     // Find the new entry action
745     auto* entryNewAction = m_mainWindow->findChild<QAction*>("actionEntryNew");
746     QVERIFY(entryNewAction->isEnabled());
747 
748     // Find the button associated with the new entry action
749     QWidget* entryNewWidget = toolBar->widgetForAction(entryNewAction);
750     QVERIFY(entryNewWidget->isVisible());
751     QVERIFY(entryNewWidget->isEnabled());
752 
753     // Click the new entry button and check that we enter edit mode
754     QTest::mouseClick(entryNewWidget, Qt::LeftButton);
755     QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
756 
757     // Add entry "test" and confirm added
758     auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
759     auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
760     QTest::keyClicks(titleEdit, "test");
761 
762     // Open the password generator
763     auto* passwordEdit = editEntryWidget->findChild<PasswordEdit*>();
764     QVERIFY(passwordEdit);
765     QTest::mouseClick(passwordEdit, Qt::LeftButton);
766 
767     QTest::keyClick(passwordEdit, Qt::Key_G, Qt::ControlModifier);
768 
769     TEST_MODAL(PasswordGeneratorWidget * pwGeneratorWidget;
770                QTRY_VERIFY(pwGeneratorWidget = m_dbWidget->findChild<PasswordGeneratorWidget*>());
771 
772                // Select Diceware
773                auto* generatedPassword = pwGeneratorWidget->findChild<QLineEdit*>("editNewPassword");
774                auto* tabWidget = pwGeneratorWidget->findChild<QTabWidget*>("tabWidget");
775                auto* dicewareWidget = pwGeneratorWidget->findChild<QWidget*>("dicewareWidget");
776                tabWidget->setCurrentWidget(dicewareWidget);
777 
778                auto* comboBoxWordList = dicewareWidget->findChild<QComboBox*>("comboBoxWordList");
779                comboBoxWordList->setCurrentText("eff_large.wordlist");
780                auto* spinBoxWordCount = dicewareWidget->findChild<QSpinBox*>("spinBoxWordCount");
781                spinBoxWordCount->setValue(6);
782 
783                // Confirm a password was generated
784                QVERIFY(!pwGeneratorWidget->getGeneratedPassword().isEmpty());
785 
786                // Verify entropy and strength
787                auto* entropyLabel = pwGeneratorWidget->findChild<QLabel*>("entropyLabel");
788                auto* strengthLabel = pwGeneratorWidget->findChild<QLabel*>("strengthLabel");
789 
790                QCOMPARE(entropyLabel->text(), QString("Entropy: 77.55 bit"));
791                QCOMPARE(strengthLabel->text(), QString("Password Quality: Good"));
792 
793                QTest::mouseClick(generatedPassword, Qt::LeftButton);
794                QTest::keyClick(generatedPassword, Qt::Key_Escape););
795 }
796 
testTotp()797 void TestGui::testTotp()
798 {
799     auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
800     auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
801 
802     QCOMPARE(entryView->model()->rowCount(), 1);
803     QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
804     QModelIndex item = entryView->model()->index(0, 1);
805     Entry* entry = entryView->entryFromIndex(item);
806     clickIndex(item, entryView, Qt::LeftButton);
807 
808     triggerAction("actionEntrySetupTotp");
809 
810     auto* setupTotpDialog = m_dbWidget->findChild<TotpSetupDialog*>("TotpSetupDialog");
811 
812     QApplication::processEvents();
813 
814     QString exampleSeed = "gezd gnbvgY 3tqojqGEZdgnb vgy3tqoJq===";
815     QString expectedFinalSeed = exampleSeed.toUpper().remove(" ").remove("=");
816     auto* seedEdit = setupTotpDialog->findChild<QLineEdit*>("seedEdit");
817     seedEdit->setText("");
818     QTest::keyClicks(seedEdit, exampleSeed);
819 
820     auto* setupTotpButtonBox = setupTotpDialog->findChild<QDialogButtonBox*>("buttonBox");
821     QTest::mouseClick(setupTotpButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
822     QTRY_VERIFY(!setupTotpDialog->isVisible());
823 
824     // Make sure the entryView is selected and active
825     entryView->activateWindow();
826     QApplication::processEvents();
827     QTRY_VERIFY(entryView->hasFocus());
828 
829     auto* entryEditAction = m_mainWindow->findChild<QAction*>("actionEntryEdit");
830     QWidget* entryEditWidget = toolBar->widgetForAction(entryEditAction);
831     QVERIFY(entryEditWidget->isVisible());
832     QVERIFY(entryEditWidget->isEnabled());
833     QTest::mouseClick(entryEditWidget, Qt::LeftButton);
834     QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
835 
836     auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
837     editEntryWidget->setCurrentPage(1);
838     auto* attrTextEdit = editEntryWidget->findChild<QPlainTextEdit*>("attributesEdit");
839     QTest::mouseClick(editEntryWidget->findChild<QAbstractButton*>("revealAttributeButton"), Qt::LeftButton);
840     QCOMPARE(attrTextEdit->toPlainText(), expectedFinalSeed);
841 
842     auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
843     QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
844 
845     triggerAction("actionEntryTotp");
846 
847     auto* totpDialog = m_dbWidget->findChild<TotpDialog*>("TotpDialog");
848     auto* totpLabel = totpDialog->findChild<QLabel*>("totpLabel");
849 
850     QCOMPARE(totpLabel->text().replace(" ", ""), entry->totp());
851 }
852 
testSearch()853 void TestGui::testSearch()
854 {
855     // Add canned entries for consistent testing
856     addCannedEntries();
857 
858     auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
859 
860     auto* searchWidget = toolBar->findChild<SearchWidget*>("SearchWidget");
861     QVERIFY(searchWidget->isEnabled());
862     auto* searchTextEdit = searchWidget->findChild<QLineEdit*>("searchEdit");
863 
864     auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
865     QVERIFY(entryView->isVisible());
866 
867     QVERIFY(searchTextEdit->isClearButtonEnabled());
868 
869     auto* helpButton = searchWidget->findChild<QAction*>("helpIcon");
870     auto* helpPanel = searchWidget->findChild<QWidget*>("SearchHelpWidget");
871     QVERIFY(helpButton->isVisible());
872     QVERIFY(!helpPanel->isVisible());
873 
874     // Enter search
875     QTest::mouseClick(searchTextEdit, Qt::LeftButton);
876     QTRY_VERIFY(searchTextEdit->hasFocus());
877     // Show/Hide search help
878     helpButton->trigger();
879     QTRY_VERIFY(helpPanel->isVisible());
880     QTest::mouseClick(searchTextEdit, Qt::LeftButton);
881     QTRY_VERIFY(helpPanel->isVisible());
882     helpButton->trigger();
883     QTRY_VERIFY(!helpPanel->isVisible());
884     // Search for "ZZZ"
885     QTest::keyClicks(searchTextEdit, "ZZZ");
886     QTRY_COMPARE(searchTextEdit->text(), QString("ZZZ"));
887     QTRY_VERIFY(m_dbWidget->isSearchActive());
888     QTRY_COMPARE(entryView->model()->rowCount(), 0);
889     // Press the search clear button
890     searchTextEdit->clear();
891     QTRY_VERIFY(searchTextEdit->text().isEmpty());
892     QTRY_VERIFY(searchTextEdit->hasFocus());
893     // Escape clears searchedit and retains focus
894     QTest::keyClicks(searchTextEdit, "ZZZ");
895     QTest::keyClick(searchTextEdit, Qt::Key_Escape);
896     QTRY_VERIFY(searchTextEdit->text().isEmpty());
897     QTRY_VERIFY(searchTextEdit->hasFocus());
898     QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
899     // Search for "some"
900     QTest::keyClicks(searchTextEdit, "some");
901     QTRY_VERIFY(m_dbWidget->isSearchActive());
902     QTRY_COMPARE(entryView->model()->rowCount(), 3);
903     // Search for "someTHING"
904     QTest::keyClicks(searchTextEdit, "THING");
905     QTRY_COMPARE(entryView->model()->rowCount(), 2);
906     // Press Down to focus on the entry view
907     QTest::keyClick(searchTextEdit, Qt::Key_Right, Qt::ControlModifier);
908     QTRY_VERIFY(searchTextEdit->hasFocus());
909     QTest::keyClick(searchTextEdit, Qt::Key_Down);
910     QTRY_VERIFY(entryView->hasFocus());
911     auto* searchedEntry = entryView->currentEntry();
912     // Restore focus using F3 key and search text selection
913     QTest::keyClick(m_mainWindow.data(), Qt::Key_F3);
914     QTRY_VERIFY(searchTextEdit->hasFocus());
915     QTRY_COMPARE(searchTextEdit->selectedText(), QString("someTHING"));
916 
917     searchedEntry->setPassword("password");
918     QClipboard* clipboard = QApplication::clipboard();
919 
920     // Attempt password copy with selected test (should fail)
921     QTest::keyClick(searchTextEdit, Qt::Key_C, Qt::ControlModifier);
922     QVERIFY(clipboard->text() != searchedEntry->password());
923     // Deselect text and confirm password copies
924     QTest::mouseClick(searchTextEdit, Qt::LeftButton);
925     QTRY_VERIFY(searchTextEdit->selectedText().isEmpty());
926     QTRY_VERIFY(searchTextEdit->hasFocus());
927     QTest::keyClick(searchTextEdit, Qt::Key_C, Qt::ControlModifier);
928     QCOMPARE(searchedEntry->password(), clipboard->text());
929     // Ensure Down focuses on entry view when search text is selected
930     QTest::keyClick(searchTextEdit, Qt::Key_A, Qt::ControlModifier);
931     QTest::keyClick(searchTextEdit, Qt::Key_Down);
932     QTRY_VERIFY(entryView->hasFocus());
933     QCOMPARE(entryView->currentEntry(), searchedEntry);
934     // Test that password copies with entry focused
935     QTest::keyClick(entryView, Qt::Key_C, Qt::ControlModifier);
936     QCOMPARE(searchedEntry->password(), clipboard->text());
937     // Refocus back to search edit
938     QTest::mouseClick(searchTextEdit, Qt::LeftButton);
939     QTRY_VERIFY(searchTextEdit->hasFocus());
940     // Test that password does not copy
941     searchTextEdit->selectAll();
942     QTest::keyClick(searchTextEdit, Qt::Key_C, Qt::ControlModifier);
943     QTRY_COMPARE(clipboard->text(), QString("someTHING"));
944 
945     // Test case sensitive search
946     searchWidget->setCaseSensitive(true);
947     QTRY_COMPARE(entryView->model()->rowCount(), 0);
948     searchWidget->setCaseSensitive(false);
949     QTRY_COMPARE(entryView->model()->rowCount(), 2);
950 
951     // Test group search
952     searchWidget->setLimitGroup(false);
953     GroupView* groupView = m_dbWidget->findChild<GroupView*>("groupView");
954     QCOMPARE(groupView->currentGroup(), m_db->rootGroup());
955     QModelIndex rootGroupIndex = groupView->model()->index(0, 0);
956     clickIndex(groupView->model()->index(0, 0, rootGroupIndex), groupView, Qt::LeftButton);
957     QCOMPARE(groupView->currentGroup()->name(), QString("General"));
958     // Selecting a group should cancel search
959     QTRY_COMPARE(entryView->model()->rowCount(), 0);
960     // Restore search
961     QTest::keyClick(m_mainWindow.data(), Qt::Key_F, Qt::ControlModifier);
962     QTest::keyClicks(searchTextEdit, "someTHING");
963     QTRY_COMPARE(entryView->model()->rowCount(), 2);
964     // Enable group limiting
965     searchWidget->setLimitGroup(true);
966     QTRY_COMPARE(entryView->model()->rowCount(), 0);
967     // Selecting another group should NOT cancel search
968     clickIndex(rootGroupIndex, groupView, Qt::LeftButton);
969     QCOMPARE(groupView->currentGroup(), m_db->rootGroup());
970     QTRY_COMPARE(entryView->model()->rowCount(), 2);
971 
972     // reset
973     searchWidget->setLimitGroup(false);
974     clickIndex(rootGroupIndex, groupView, Qt::LeftButton);
975     QCOMPARE(groupView->currentGroup(), m_db->rootGroup());
976     QVERIFY(!m_dbWidget->isSearchActive());
977 
978     // Try to edit the first entry from the search view
979     // Refocus back to search edit
980     QTest::mouseClick(searchTextEdit, Qt::LeftButton);
981     QTRY_VERIFY(searchTextEdit->hasFocus());
982     QTest::keyClicks(searchTextEdit, "someTHING");
983     QTRY_VERIFY(m_dbWidget->isSearchActive());
984 
985     QModelIndex item = entryView->model()->index(0, 1);
986     Entry* entry = entryView->entryFromIndex(item);
987     QTest::keyClick(searchTextEdit, Qt::Key_Return);
988     QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
989 
990     // Perform the edit and save it
991     EditEntryWidget* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
992     QLineEdit* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
993     QString origTitle = titleEdit->text();
994     QTest::keyClicks(titleEdit, "_edited");
995     QDialogButtonBox* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
996     QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
997 
998     // Confirm the edit was made and we are back in search mode
999     QTRY_VERIFY(m_dbWidget->isSearchActive());
1000     QCOMPARE(entry->title(), origTitle.append("_edited"));
1001 
1002     // Cancel search, should return to normal view
1003     QTest::keyClick(m_mainWindow.data(), Qt::Key_Escape);
1004     QTRY_COMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
1005 }
1006 
testDeleteEntry()1007 void TestGui::testDeleteEntry()
1008 {
1009     // Add canned entries for consistent testing
1010     addCannedEntries();
1011 
1012     auto* groupView = m_dbWidget->findChild<GroupView*>("groupView");
1013     auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
1014     auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
1015     auto* entryDeleteAction = m_mainWindow->findChild<QAction*>("actionEntryDelete");
1016     QWidget* entryDeleteWidget = toolBar->widgetForAction(entryDeleteAction);
1017     entryView->setFocus();
1018 
1019     // Move one entry to the recycling bin
1020     QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
1021     clickIndex(entryView->model()->index(1, 1), entryView, Qt::LeftButton);
1022     QVERIFY(entryDeleteWidget->isVisible());
1023     QVERIFY(entryDeleteWidget->isEnabled());
1024     QVERIFY(!m_db->metadata()->recycleBin());
1025 
1026     MessageBox::setNextAnswer(MessageBox::Move);
1027     QTest::mouseClick(entryDeleteWidget, Qt::LeftButton);
1028 
1029     QCOMPARE(entryView->model()->rowCount(), 3);
1030     QCOMPARE(m_db->metadata()->recycleBin()->entries().size(), 1);
1031 
1032     // Select multiple entries and move them to the recycling bin
1033     clickIndex(entryView->model()->index(1, 1), entryView, Qt::LeftButton);
1034     clickIndex(entryView->model()->index(2, 1), entryView, Qt::LeftButton, Qt::ControlModifier);
1035     QCOMPARE(entryView->selectionModel()->selectedRows().size(), 2);
1036 
1037     MessageBox::setNextAnswer(MessageBox::Cancel);
1038     QTest::mouseClick(entryDeleteWidget, Qt::LeftButton);
1039     QCOMPARE(entryView->model()->rowCount(), 3);
1040     QCOMPARE(m_db->metadata()->recycleBin()->entries().size(), 1);
1041 
1042     MessageBox::setNextAnswer(MessageBox::Move);
1043     QTest::mouseClick(entryDeleteWidget, Qt::LeftButton);
1044     QCOMPARE(entryView->model()->rowCount(), 1);
1045     QCOMPARE(m_db->metadata()->recycleBin()->entries().size(), 3);
1046 
1047     // Go to the recycling bin
1048     QCOMPARE(groupView->currentGroup(), m_db->rootGroup());
1049     QModelIndex rootGroupIndex = groupView->model()->index(0, 0);
1050     clickIndex(groupView->model()->index(groupView->model()->rowCount(rootGroupIndex) - 1, 0, rootGroupIndex),
1051                groupView,
1052                Qt::LeftButton);
1053     QCOMPARE(groupView->currentGroup()->name(), m_db->metadata()->recycleBin()->name());
1054 
1055     // Delete one entry from the bin
1056     clickIndex(entryView->model()->index(0, 1), entryView, Qt::LeftButton);
1057     MessageBox::setNextAnswer(MessageBox::Cancel);
1058     QTest::mouseClick(entryDeleteWidget, Qt::LeftButton);
1059     QCOMPARE(entryView->model()->rowCount(), 3);
1060     QCOMPARE(m_db->metadata()->recycleBin()->entries().size(), 3);
1061 
1062     MessageBox::setNextAnswer(MessageBox::Delete);
1063     QTest::mouseClick(entryDeleteWidget, Qt::LeftButton);
1064     QCOMPARE(entryView->model()->rowCount(), 2);
1065     QCOMPARE(m_db->metadata()->recycleBin()->entries().size(), 2);
1066 
1067     // Select the remaining entries and delete them
1068     clickIndex(entryView->model()->index(0, 1), entryView, Qt::LeftButton);
1069     clickIndex(entryView->model()->index(1, 1), entryView, Qt::LeftButton, Qt::ControlModifier);
1070     MessageBox::setNextAnswer(MessageBox::Delete);
1071     QTest::mouseClick(entryDeleteWidget, Qt::LeftButton);
1072     QCOMPARE(entryView->model()->rowCount(), 0);
1073     QCOMPARE(m_db->metadata()->recycleBin()->entries().size(), 0);
1074 
1075     // Ensure the entry preview widget shows the recycling group since all entries are deleted
1076     auto* previewWidget = m_dbWidget->findChild<EntryPreviewWidget*>("previewWidget");
1077     QVERIFY(previewWidget);
1078     auto* groupTitleLabel = previewWidget->findChild<QLabel*>("groupTitleLabel");
1079     QVERIFY(groupTitleLabel);
1080 
1081     QTRY_VERIFY(groupTitleLabel->isVisible());
1082     QVERIFY(groupTitleLabel->text().contains(m_db->metadata()->recycleBin()->name()));
1083 
1084     // Go back to the root group
1085     clickIndex(groupView->model()->index(0, 0), groupView, Qt::LeftButton);
1086     QCOMPARE(groupView->currentGroup(), m_db->rootGroup());
1087 }
1088 
testCloneEntry()1089 void TestGui::testCloneEntry()
1090 {
1091     auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
1092     entryView->setFocus();
1093 
1094     QCOMPARE(entryView->model()->rowCount(), 1);
1095 
1096     QModelIndex item = entryView->model()->index(0, 1);
1097     Entry* entryOrg = entryView->entryFromIndex(item);
1098     clickIndex(item, entryView, Qt::LeftButton);
1099 
1100     triggerAction("actionEntryClone");
1101 
1102     auto* cloneDialog = m_dbWidget->findChild<CloneDialog*>("CloneDialog");
1103     auto* cloneButtonBox = cloneDialog->findChild<QDialogButtonBox*>("buttonBox");
1104     QTest::mouseClick(cloneButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
1105 
1106     QCOMPARE(entryView->model()->rowCount(), 2);
1107     Entry* entryClone = entryView->entryFromIndex(entryView->model()->index(1, 1));
1108     QVERIFY(entryOrg->uuid() != entryClone->uuid());
1109     QCOMPARE(entryClone->title(), entryOrg->title() + QString(" - Clone"));
1110 }
1111 
testEntryPlaceholders()1112 void TestGui::testEntryPlaceholders()
1113 {
1114     auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
1115     auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
1116 
1117     // Find the new entry action
1118     auto* entryNewAction = m_mainWindow->findChild<QAction*>("actionEntryNew");
1119     QVERIFY(entryNewAction->isEnabled());
1120 
1121     // Find the button associated with the new entry action
1122     QWidget* entryNewWidget = toolBar->widgetForAction(entryNewAction);
1123     QVERIFY(entryNewWidget->isVisible());
1124     QVERIFY(entryNewWidget->isEnabled());
1125 
1126     // Click the new entry button and check that we enter edit mode
1127     QTest::mouseClick(entryNewWidget, Qt::LeftButton);
1128     QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
1129 
1130     // Add entry "test" and confirm added
1131     auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
1132     auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
1133     QTest::keyClicks(titleEdit, "test");
1134     QComboBox* usernameComboBox = editEntryWidget->findChild<QComboBox*>("usernameComboBox");
1135     QTest::keyClicks(usernameComboBox, "john");
1136     QLineEdit* urlEdit = editEntryWidget->findChild<QLineEdit*>("urlEdit");
1137     QTest::keyClicks(urlEdit, "{TITLE}.{USERNAME}");
1138     auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
1139     QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
1140 
1141     QCOMPARE(entryView->model()->rowCount(), 2);
1142 
1143     QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
1144     QModelIndex item = entryView->model()->index(1, 1);
1145     Entry* entry = entryView->entryFromIndex(item);
1146 
1147     QCOMPARE(entry->title(), QString("test"));
1148     QCOMPARE(entry->url(), QString("{TITLE}.{USERNAME}"));
1149 
1150     // Test password copy
1151     QClipboard* clipboard = QApplication::clipboard();
1152     m_dbWidget->copyURL();
1153     QTRY_COMPARE(clipboard->text(), QString("test.john"));
1154 }
1155 
testDragAndDropEntry()1156 void TestGui::testDragAndDropEntry()
1157 {
1158     auto entryView = m_dbWidget->findChild<EntryView*>("entryView");
1159     auto groupView = m_dbWidget->findChild<GroupView*>("groupView");
1160     auto groupModel = qobject_cast<GroupModel*>(groupView->model());
1161 
1162     QModelIndex sourceIndex = entryView->model()->index(0, 1);
1163     QModelIndex targetIndex = groupModel->index(0, 0, groupModel->index(0, 0));
1164     QVERIFY(sourceIndex.isValid());
1165     QVERIFY(targetIndex.isValid());
1166     auto targetGroup = groupModel->groupFromIndex(targetIndex);
1167 
1168     QMimeData mimeData;
1169     QByteArray encoded;
1170     QDataStream stream(&encoded, QIODevice::WriteOnly);
1171 
1172     auto entry = entryView->entryFromIndex(sourceIndex);
1173     stream << entry->group()->database()->uuid() << entry->uuid();
1174     mimeData.setData("application/x-keepassx-entry", encoded);
1175 
1176     // Test Copy, UUID should change, history remain
1177     QVERIFY(groupModel->dropMimeData(&mimeData, Qt::CopyAction, -1, 0, targetIndex));
1178     // Find the copied entry
1179     auto newEntry = targetGroup->findEntryByPath(entry->title());
1180     QVERIFY(newEntry);
1181     QVERIFY(entry->uuid() != newEntry->uuid());
1182     QCOMPARE(entry->historyItems().count(), newEntry->historyItems().count());
1183 
1184     encoded.clear();
1185     entry = entryView->entryFromIndex(sourceIndex);
1186     auto history = entry->historyItems().count();
1187     auto uuid = entry->uuid();
1188     stream << entry->group()->database()->uuid() << entry->uuid();
1189     mimeData.setData("application/x-keepassx-entry", encoded);
1190 
1191     // Test Move, entry pointer should remain the same
1192     QCOMPARE(entry->group()->name(), QString("NewDatabase"));
1193     QVERIFY(groupModel->dropMimeData(&mimeData, Qt::MoveAction, -1, 0, targetIndex));
1194     QCOMPARE(entry->group()->name(), QString("General"));
1195     QCOMPARE(entry->uuid(), uuid);
1196     QCOMPARE(entry->historyItems().count(), history);
1197 }
1198 
testDragAndDropGroup()1199 void TestGui::testDragAndDropGroup()
1200 {
1201     QAbstractItemModel* groupModel = m_dbWidget->findChild<GroupView*>("groupView")->model();
1202     QModelIndex rootIndex = groupModel->index(0, 0);
1203 
1204     dragAndDropGroup(groupModel->index(0, 0, rootIndex), groupModel->index(1, 0, rootIndex), -1, true, "Windows", 0);
1205 
1206     // dropping parent on child is supposed to fail
1207     dragAndDropGroup(groupModel->index(0, 0, rootIndex),
1208                      groupModel->index(0, 0, groupModel->index(0, 0, rootIndex)),
1209                      -1,
1210                      false,
1211                      "NewDatabase",
1212                      0);
1213 
1214     dragAndDropGroup(groupModel->index(1, 0, rootIndex), rootIndex, 0, true, "NewDatabase", 0);
1215 
1216     dragAndDropGroup(groupModel->index(0, 0, rootIndex), rootIndex, -1, true, "NewDatabase", 4);
1217 }
1218 
testSaveAs()1219 void TestGui::testSaveAs()
1220 {
1221     QFileInfo fileInfo(m_dbFilePath);
1222     QDateTime lastModified = fileInfo.lastModified();
1223 
1224     m_db->metadata()->setName("testSaveAs");
1225 
1226     // open temporary file so it creates a filename
1227     TemporaryFile tmpFile;
1228     QVERIFY(tmpFile.open());
1229     QString tmpFileName = tmpFile.fileName();
1230     tmpFile.remove();
1231 
1232     fileDialog()->setNextFileName(tmpFileName);
1233 
1234     triggerAction("actionDatabaseSaveAs");
1235 
1236     QCOMPARE(m_tabWidget->tabName(m_tabWidget->currentIndex()), QString("testSaveAs"));
1237 
1238     checkDatabase(tmpFileName);
1239 
1240     fileInfo.refresh();
1241     QCOMPARE(fileInfo.lastModified(), lastModified);
1242     tmpFile.remove();
1243 }
1244 
testSaveBackup()1245 void TestGui::testSaveBackup()
1246 {
1247     m_db->metadata()->setName("testSaveBackup");
1248 
1249     QFileInfo fileInfo(m_dbFilePath);
1250     QDateTime lastModified = fileInfo.lastModified();
1251 
1252     // open temporary file so it creates a filename
1253     TemporaryFile tmpFile;
1254     QVERIFY(tmpFile.open());
1255     QString tmpFileName = tmpFile.fileName();
1256     tmpFile.remove();
1257 
1258     // wait for modified timer
1259     QTRY_COMPARE(m_tabWidget->tabText(m_tabWidget->currentIndex()), QString("testSaveBackup*"));
1260 
1261     fileDialog()->setNextFileName(tmpFileName);
1262 
1263     triggerAction("actionDatabaseSaveBackup");
1264 
1265     QCOMPARE(m_tabWidget->tabName(m_tabWidget->currentIndex()), QString("testSaveBackup*"));
1266 
1267     checkDatabase(tmpFileName);
1268 
1269     fileInfo.refresh();
1270     QCOMPARE(fileInfo.lastModified(), lastModified);
1271     tmpFile.remove();
1272 }
1273 
testSave()1274 void TestGui::testSave()
1275 {
1276     m_db->metadata()->setName("testSave");
1277 
1278     // wait for modified timer
1279     QTRY_COMPARE(m_tabWidget->tabText(m_tabWidget->currentIndex()), QString("testSave*"));
1280 
1281     triggerAction("actionDatabaseSave");
1282     QCOMPARE(m_tabWidget->tabName(m_tabWidget->currentIndex()), QString("testSave"));
1283 
1284     checkDatabase();
1285 }
1286 
testDatabaseSettings()1287 void TestGui::testDatabaseSettings()
1288 {
1289     m_db->metadata()->setName("testDatabaseSettings");
1290     triggerAction("actionDatabaseSettings");
1291     auto* dbSettingsDialog = m_dbWidget->findChild<QWidget*>("databaseSettingsDialog");
1292     auto* transformRoundsSpinBox = dbSettingsDialog->findChild<QSpinBox*>("transformRoundsSpinBox");
1293     auto advancedToggle = dbSettingsDialog->findChild<QCheckBox*>("advancedSettingsToggle");
1294 
1295     advancedToggle->setChecked(true);
1296     QApplication::processEvents();
1297 
1298     QVERIFY(transformRoundsSpinBox != nullptr);
1299     transformRoundsSpinBox->setValue(123456);
1300     QTest::keyClick(transformRoundsSpinBox, Qt::Key_Enter);
1301     // wait for modified timer
1302     QTRY_COMPARE(m_tabWidget->tabText(m_tabWidget->currentIndex()), QString("testDatabaseSettings*"));
1303     QCOMPARE(m_db->kdf()->rounds(), 123456);
1304 
1305     triggerAction("actionDatabaseSave");
1306     QCOMPARE(m_tabWidget->tabText(m_tabWidget->currentIndex()), QString("testDatabaseSettings"));
1307 
1308     advancedToggle->setChecked(false);
1309     QApplication::processEvents();
1310 
1311     checkDatabase();
1312 }
1313 
testKeePass1Import()1314 void TestGui::testKeePass1Import()
1315 {
1316     fileDialog()->setNextFileName(QString(KEEPASSX_TEST_DATA_DIR).append("/basic.kdb"));
1317     triggerAction("actionImportKeePass1");
1318 
1319     auto* keepass1OpenWidget = m_tabWidget->currentDatabaseWidget()->findChild<QWidget*>("keepass1OpenWidget");
1320     auto* editPassword = keepass1OpenWidget->findChild<QLineEdit*>("editPassword");
1321     QVERIFY(editPassword);
1322 
1323     QTest::keyClicks(editPassword, "masterpw");
1324     QTest::keyClick(editPassword, Qt::Key_Enter);
1325 
1326     QTRY_COMPARE(m_tabWidget->count(), 2);
1327     QTRY_COMPARE(m_tabWidget->tabName(m_tabWidget->currentIndex()), QString("basic [New Database]*"));
1328 
1329     // Close the KeePass1 Database
1330     MessageBox::setNextAnswer(MessageBox::No);
1331     triggerAction("actionDatabaseClose");
1332     QApplication::processEvents();
1333 }
1334 
testDatabaseLocking()1335 void TestGui::testDatabaseLocking()
1336 {
1337     QString origDbName = m_tabWidget->tabText(0);
1338 
1339     MessageBox::setNextAnswer(MessageBox::Cancel);
1340     triggerAction("actionLockDatabases");
1341 
1342     QCOMPARE(m_tabWidget->tabName(0), origDbName + " [Locked]");
1343 
1344     auto* actionDatabaseMerge = m_mainWindow->findChild<QAction*>("actionDatabaseMerge", Qt::FindChildrenRecursively);
1345     QCOMPARE(actionDatabaseMerge->isEnabled(), false);
1346     auto* actionDatabaseSave = m_mainWindow->findChild<QAction*>("actionDatabaseSave", Qt::FindChildrenRecursively);
1347     QCOMPARE(actionDatabaseSave->isEnabled(), false);
1348 
1349     DatabaseWidget* dbWidget = m_tabWidget->currentDatabaseWidget();
1350     QVERIFY(dbWidget->isLocked());
1351     auto* unlockDatabaseWidget = dbWidget->findChild<QWidget*>("databaseOpenWidget");
1352     QWidget* editPassword = unlockDatabaseWidget->findChild<QLineEdit*>("editPassword");
1353     QVERIFY(editPassword);
1354 
1355     QTest::keyClicks(editPassword, "a");
1356     QTest::keyClick(editPassword, Qt::Key_Enter);
1357 
1358     QVERIFY(!dbWidget->isLocked());
1359     QCOMPARE(m_tabWidget->tabName(0), origDbName);
1360 
1361     actionDatabaseMerge = m_mainWindow->findChild<QAction*>("actionDatabaseMerge", Qt::FindChildrenRecursively);
1362     QCOMPARE(actionDatabaseMerge->isEnabled(), true);
1363 }
1364 
testDragAndDropKdbxFiles()1365 void TestGui::testDragAndDropKdbxFiles()
1366 {
1367     const int openedDatabasesCount = m_tabWidget->count();
1368 
1369     const QString badDatabaseFilePath(QString(KEEPASSX_TEST_DATA_DIR).append("/NotDatabase.notkdbx"));
1370     QMimeData badMimeData;
1371     badMimeData.setUrls({QUrl::fromLocalFile(badDatabaseFilePath)});
1372     QDragEnterEvent badDragEvent(QPoint(1, 1), Qt::LinkAction, &badMimeData, Qt::LeftButton, Qt::NoModifier);
1373     qApp->notify(m_mainWindow.data(), &badDragEvent);
1374     QCOMPARE(badDragEvent.isAccepted(), false);
1375 
1376     QDropEvent badDropEvent(QPoint(1, 1), Qt::LinkAction, &badMimeData, Qt::LeftButton, Qt::NoModifier);
1377     qApp->notify(m_mainWindow.data(), &badDropEvent);
1378     QCOMPARE(badDropEvent.isAccepted(), false);
1379 
1380     QCOMPARE(m_tabWidget->count(), openedDatabasesCount);
1381 
1382     QMimeData goodMimeData;
1383     goodMimeData.setUrls({QUrl::fromLocalFile(dbFileName)});
1384     QDragEnterEvent goodDragEvent(QPoint(1, 1), Qt::LinkAction, &goodMimeData, Qt::LeftButton, Qt::NoModifier);
1385     qApp->notify(m_mainWindow.data(), &goodDragEvent);
1386     QCOMPARE(goodDragEvent.isAccepted(), true);
1387 
1388     QDropEvent goodDropEvent(QPoint(1, 1), Qt::LinkAction, &goodMimeData, Qt::LeftButton, Qt::NoModifier);
1389     qApp->notify(m_mainWindow.data(), &goodDropEvent);
1390     QCOMPARE(goodDropEvent.isAccepted(), true);
1391 
1392     QCOMPARE(m_tabWidget->count(), openedDatabasesCount + 1);
1393 
1394     MessageBox::setNextAnswer(MessageBox::No);
1395     triggerAction("actionDatabaseClose");
1396 
1397     QTRY_COMPARE(m_tabWidget->count(), openedDatabasesCount);
1398 }
1399 
testSortGroups()1400 void TestGui::testSortGroups()
1401 {
1402     auto* editGroupWidget = m_dbWidget->findChild<EditGroupWidget*>("editGroupWidget");
1403     auto* nameEdit = editGroupWidget->findChild<QLineEdit*>("editName");
1404     auto* editGroupWidgetButtonBox = editGroupWidget->findChild<QDialogButtonBox*>("buttonBox");
1405 
1406     // Create some sub-groups
1407     Group* rootGroup = m_db->rootGroup();
1408     Group* internetGroup = rootGroup->findGroupByPath("Internet");
1409     m_dbWidget->groupView()->setCurrentGroup(internetGroup);
1410     m_dbWidget->createGroup();
1411     QTest::keyClicks(nameEdit, "Google");
1412     QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
1413     m_dbWidget->groupView()->setCurrentGroup(internetGroup);
1414     m_dbWidget->createGroup();
1415     QTest::keyClicks(nameEdit, "eBay");
1416     QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
1417     m_dbWidget->groupView()->setCurrentGroup(internetGroup);
1418     m_dbWidget->createGroup();
1419     QTest::keyClicks(nameEdit, "Amazon");
1420     QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
1421     m_dbWidget->groupView()->setCurrentGroup(internetGroup);
1422     m_dbWidget->createGroup();
1423     QTest::keyClicks(nameEdit, "Facebook");
1424     QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
1425     m_dbWidget->groupView()->setCurrentGroup(rootGroup);
1426 
1427     triggerAction("actionGroupSortAsc");
1428     QList<Group*> children = rootGroup->children();
1429     QCOMPARE(children[0]->name(), QString("eMail"));
1430     QCOMPARE(children[1]->name(), QString("General"));
1431     QCOMPARE(children[2]->name(), QString("Homebanking"));
1432     QCOMPARE(children[3]->name(), QString("Internet"));
1433     QCOMPARE(children[4]->name(), QString("Network"));
1434     QCOMPARE(children[5]->name(), QString("Windows"));
1435     QList<Group*> subChildren = internetGroup->children();
1436     QCOMPARE(subChildren[0]->name(), QString("Amazon"));
1437     QCOMPARE(subChildren[1]->name(), QString("eBay"));
1438     QCOMPARE(subChildren[2]->name(), QString("Facebook"));
1439     QCOMPARE(subChildren[3]->name(), QString("Google"));
1440 
1441     triggerAction("actionGroupSortDesc");
1442     children = rootGroup->children();
1443     QCOMPARE(children[0]->name(), QString("Windows"));
1444     QCOMPARE(children[1]->name(), QString("Network"));
1445     QCOMPARE(children[2]->name(), QString("Internet"));
1446     QCOMPARE(children[3]->name(), QString("Homebanking"));
1447     QCOMPARE(children[4]->name(), QString("General"));
1448     QCOMPARE(children[5]->name(), QString("eMail"));
1449     subChildren = internetGroup->children();
1450     QCOMPARE(subChildren[0]->name(), QString("Google"));
1451     QCOMPARE(subChildren[1]->name(), QString("Facebook"));
1452     QCOMPARE(subChildren[2]->name(), QString("eBay"));
1453     QCOMPARE(subChildren[3]->name(), QString("Amazon"));
1454 
1455     m_dbWidget->groupView()->setCurrentGroup(internetGroup);
1456     triggerAction("actionGroupSortAsc");
1457     children = rootGroup->children();
1458     QCOMPARE(children[0]->name(), QString("Windows"));
1459     QCOMPARE(children[1]->name(), QString("Network"));
1460     QCOMPARE(children[2]->name(), QString("Internet"));
1461     QCOMPARE(children[3]->name(), QString("Homebanking"));
1462     QCOMPARE(children[4]->name(), QString("General"));
1463     QCOMPARE(children[5]->name(), QString("eMail"));
1464     subChildren = internetGroup->children();
1465     QCOMPARE(subChildren[0]->name(), QString("Amazon"));
1466     QCOMPARE(subChildren[1]->name(), QString("eBay"));
1467     QCOMPARE(subChildren[2]->name(), QString("Facebook"));
1468     QCOMPARE(subChildren[3]->name(), QString("Google"));
1469 
1470     m_dbWidget->groupView()->setCurrentGroup(rootGroup);
1471     triggerAction("actionGroupSortAsc");
1472     m_dbWidget->groupView()->setCurrentGroup(internetGroup);
1473     triggerAction("actionGroupSortDesc");
1474     children = rootGroup->children();
1475     QCOMPARE(children[0]->name(), QString("eMail"));
1476     QCOMPARE(children[1]->name(), QString("General"));
1477     QCOMPARE(children[2]->name(), QString("Homebanking"));
1478     QCOMPARE(children[3]->name(), QString("Internet"));
1479     QCOMPARE(children[4]->name(), QString("Network"));
1480     QCOMPARE(children[5]->name(), QString("Windows"));
1481     subChildren = internetGroup->children();
1482     QCOMPARE(subChildren[0]->name(), QString("Google"));
1483     QCOMPARE(subChildren[1]->name(), QString("Facebook"));
1484     QCOMPARE(subChildren[2]->name(), QString("eBay"));
1485     QCOMPARE(subChildren[3]->name(), QString("Amazon"));
1486 }
1487 
testTrayRestoreHide()1488 void TestGui::testTrayRestoreHide()
1489 {
1490     if (!QSystemTrayIcon::isSystemTrayAvailable()) {
1491         QSKIP("QSystemTrayIcon::isSystemTrayAvailable() = false, skipping tray restore/hide test...");
1492     }
1493 
1494     m_mainWindow->hideWindow();
1495     QVERIFY(!m_mainWindow->isVisible());
1496 
1497     auto* trayIcon = m_mainWindow->findChild<QSystemTrayIcon*>();
1498     QVERIFY(trayIcon);
1499 
1500     trayIcon->activated(QSystemTrayIcon::Trigger);
1501     QTRY_VERIFY(m_mainWindow->isVisible());
1502 
1503     trayIcon->activated(QSystemTrayIcon::Trigger);
1504     QTRY_VERIFY(!m_mainWindow->isVisible());
1505 
1506     trayIcon->activated(QSystemTrayIcon::MiddleClick);
1507     QTRY_VERIFY(m_mainWindow->isVisible());
1508 
1509     trayIcon->activated(QSystemTrayIcon::MiddleClick);
1510     QTRY_VERIFY(!m_mainWindow->isVisible());
1511 
1512     trayIcon->activated(QSystemTrayIcon::DoubleClick);
1513     QTRY_VERIFY(m_mainWindow->isVisible());
1514 
1515     trayIcon->activated(QSystemTrayIcon::DoubleClick);
1516     QTRY_VERIFY(!m_mainWindow->isVisible());
1517 
1518     // Ensure window is visible at the end
1519     trayIcon->activated(QSystemTrayIcon::DoubleClick);
1520     QTRY_VERIFY(m_mainWindow->isVisible());
1521 }
1522 
testAutoType()1523 void TestGui::testAutoType()
1524 {
1525     // Clear entries from root group to guarantee order
1526     for (Entry* entry : m_db->rootGroup()->entries()) {
1527         m_db->rootGroup()->removeEntry(entry);
1528     }
1529     Tools::wait(150);
1530 
1531     // 1. Create an entry with Auto-Type disabled
1532 
1533     // 1.a) Click the new entry button and set the title
1534     auto* entryNewAction = m_mainWindow->findChild<QAction*>("actionEntryNew");
1535     QVERIFY(entryNewAction->isEnabled());
1536 
1537     auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
1538     QVERIFY(toolBar);
1539 
1540     QWidget* entryNewWidget = toolBar->widgetForAction(entryNewAction);
1541     QVERIFY(entryNewWidget->isVisible());
1542     QVERIFY(entryNewWidget->isEnabled());
1543 
1544     QTest::mouseClick(entryNewWidget, Qt::LeftButton);
1545     QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
1546 
1547     auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
1548     QVERIFY(editEntryWidget);
1549 
1550     auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
1551     QVERIFY(titleEdit);
1552 
1553     QTest::keyClicks(titleEdit, "1. Entry With Disabled Auto-Type");
1554 
1555     auto* usernameComboBox = editEntryWidget->findChild<QComboBox*>("usernameComboBox");
1556     QVERIFY(usernameComboBox);
1557 
1558     QTest::mouseClick(usernameComboBox, Qt::LeftButton);
1559     QTest::keyClicks(usernameComboBox, "AutocompletionUsername");
1560 
1561     // 1.b) Uncheck Auto-Type checkbox
1562     editEntryWidget->setCurrentPage(3);
1563     auto* enableAutoTypeButton = editEntryWidget->findChild<QCheckBox*>("enableButton");
1564     QVERIFY(enableAutoTypeButton);
1565     QVERIFY(enableAutoTypeButton->isVisible());
1566     QVERIFY(enableAutoTypeButton->isEnabled());
1567 
1568     enableAutoTypeButton->click();
1569     QVERIFY(!enableAutoTypeButton->isChecked());
1570 
1571     // 1.c) Save changes
1572     editEntryWidget->setCurrentPage(0);
1573     auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
1574     QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
1575 
1576     // 2. Create an entry with default/inherited Auto-Type sequence
1577 
1578     // 2.a) Click the new entry button and set the title
1579     QTest::mouseClick(entryNewWidget, Qt::LeftButton);
1580     QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
1581     QTest::keyClicks(titleEdit, "2. Entry With Default Auto-Type Sequence");
1582     QTest::mouseClick(usernameComboBox, Qt::LeftButton);
1583     QTest::keyClicks(usernameComboBox, "AutocompletionUsername");
1584 
1585     // 2.b) Confirm AutoType is enabled and default
1586     editEntryWidget->setCurrentPage(3);
1587     QVERIFY(enableAutoTypeButton->isChecked());
1588     auto* inheritSequenceButton = editEntryWidget->findChild<QRadioButton*>("inheritSequenceButton");
1589     QVERIFY(inheritSequenceButton->isChecked());
1590 
1591     // 2.c) Save changes
1592     editEntryWidget->setCurrentPage(0);
1593     QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
1594 
1595     // 3. Create an entry with custom Auto-Type sequence
1596 
1597     // 3.a) Click the new entry button and set the title
1598     QTest::mouseClick(entryNewWidget, Qt::LeftButton);
1599     QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
1600     QTest::keyClicks(titleEdit, "3. Entry With Custom Auto-Type Sequence");
1601     QTest::mouseClick(usernameComboBox, Qt::LeftButton);
1602     QTest::keyClicks(usernameComboBox, "AutocompletionUsername");
1603 
1604     // 3.b) Confirm AutoType is enabled and set custom sequence
1605     editEntryWidget->setCurrentPage(3);
1606     QVERIFY(enableAutoTypeButton->isChecked());
1607     auto* customSequenceButton = editEntryWidget->findChild<QRadioButton*>("customSequenceButton");
1608     QTest::mouseClick(customSequenceButton, Qt::LeftButton);
1609     QVERIFY(customSequenceButton->isChecked());
1610     QVERIFY(!inheritSequenceButton->isChecked());
1611     auto* sequenceEdit = editEntryWidget->findChild<QLineEdit*>("sequenceEdit");
1612     QVERIFY(sequenceEdit);
1613     sequenceEdit->setFocus();
1614     QTRY_VERIFY(sequenceEdit->hasFocus());
1615     QTest::keyClicks(sequenceEdit, "{USERNAME}{TAB}{TAB}{PASSWORD}{ENTER}");
1616 
1617     // 3.c) Save changes
1618     editEntryWidget->setCurrentPage(0);
1619     QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
1620     QApplication::processEvents();
1621 
1622     // Check total number of entries matches expected
1623     auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
1624     QVERIFY(entryView);
1625     QTRY_COMPARE(entryView->model()->rowCount(), 3);
1626 
1627     // Sort entries by title
1628     entryView->sortByColumn(1, Qt::AscendingOrder);
1629 
1630     // Select first entry
1631     entryView->selectionModel()->clearSelection();
1632     QModelIndex entryIndex = entryView->model()->index(0, 0);
1633     entryView->selectionModel()->select(entryIndex, QItemSelectionModel::Rows | QItemSelectionModel::Select);
1634 
1635     auto* entryPreviewWidget = m_dbWidget->findChild<EntryPreviewWidget*>("previewWidget");
1636     QVERIFY(entryPreviewWidget->isVisible());
1637 
1638     // Check that the Autotype tab in entry preview pane is disabled for entry with disabled Auto-Type
1639     auto* entryAutotypeTab = entryPreviewWidget->findChild<QWidget*>("entryAutotypeTab");
1640     QVERIFY(!entryAutotypeTab->isEnabled());
1641 
1642     // Check that Auto-Type is disabled in the actual entry model as well
1643     Entry* entry = entryView->entryFromIndex(entryIndex);
1644     QVERIFY(!entry->autoTypeEnabled());
1645 
1646     // Select second entry
1647     entryView->selectionModel()->clearSelection();
1648     entryIndex = entryView->model()->index(1, 0);
1649     entryView->selectionModel()->select(entryIndex, QItemSelectionModel::Rows | QItemSelectionModel::Select);
1650     QVERIFY(entryPreviewWidget->isVisible());
1651 
1652     // Check that the Autotype tab in entry preview pane is enabled for entry with default Auto-Type sequence;
1653     QVERIFY(entryAutotypeTab->isEnabled());
1654 
1655     // Check that Auto-Type is enabled in the actual entry model as well
1656     entry = entryView->entryFromIndex(entryIndex);
1657     QVERIFY(entry->autoTypeEnabled());
1658 
1659     // Select third entry
1660     entryView->selectionModel()->clearSelection();
1661     entryIndex = entryView->model()->index(2, 0);
1662     entryView->selectionModel()->select(entryIndex, QItemSelectionModel::Rows | QItemSelectionModel::Select);
1663     QVERIFY(entryPreviewWidget->isVisible());
1664 
1665     // Check that the Autotype tab in entry preview pane is enabled for entry with custom Auto-Type sequence
1666     QVERIFY(entryAutotypeTab->isEnabled());
1667 
1668     // Check that Auto-Type is enabled in the actual entry model as well
1669     entry = entryView->entryFromIndex(entryIndex);
1670     QVERIFY(entry->autoTypeEnabled());
1671 
1672     // De-select third entry
1673     entryView->selectionModel()->clearSelection();
1674 }
1675 
addCannedEntries()1676 void TestGui::addCannedEntries()
1677 {
1678     // Find buttons
1679     auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
1680     QWidget* entryNewWidget = toolBar->widgetForAction(m_mainWindow->findChild<QAction*>("actionEntryNew"));
1681     auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
1682     auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
1683     auto* passwordEdit = editEntryWidget->findChild<QLineEdit*>("passwordEdit");
1684 
1685     // Add entry "test" and confirm added
1686     QTest::mouseClick(entryNewWidget, Qt::LeftButton);
1687     QTest::keyClicks(titleEdit, "test");
1688     auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
1689     QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
1690 
1691     // Add entry "something 2"
1692     QTest::mouseClick(entryNewWidget, Qt::LeftButton);
1693     QTest::keyClicks(titleEdit, "something 2");
1694     QTest::keyClicks(passwordEdit, "something 2");
1695     QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
1696 
1697     // Add entry "something 3"
1698     QTest::mouseClick(entryNewWidget, Qt::LeftButton);
1699     QTest::keyClicks(titleEdit, "something 3");
1700     QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
1701 }
1702 
checkDatabase(QString dbFileName)1703 void TestGui::checkDatabase(QString dbFileName)
1704 {
1705     if (dbFileName.isEmpty()) {
1706         dbFileName = m_dbFilePath;
1707     }
1708 
1709     auto key = QSharedPointer<CompositeKey>::create();
1710     key->addKey(QSharedPointer<PasswordKey>::create("a"));
1711     auto dbSaved = QSharedPointer<Database>::create();
1712     QVERIFY(dbSaved->open(dbFileName, key, nullptr, false));
1713     QCOMPARE(dbSaved->metadata()->name(), m_db->metadata()->name());
1714 }
1715 
triggerAction(const QString & name)1716 void TestGui::triggerAction(const QString& name)
1717 {
1718     auto* action = m_mainWindow->findChild<QAction*>(name);
1719     QVERIFY(action);
1720     QVERIFY(action->isEnabled());
1721     action->trigger();
1722     QApplication::processEvents();
1723 }
1724 
dragAndDropGroup(const QModelIndex & sourceIndex,const QModelIndex & targetIndex,int row,bool expectedResult,const QString & expectedParentName,int expectedPos)1725 void TestGui::dragAndDropGroup(const QModelIndex& sourceIndex,
1726                                const QModelIndex& targetIndex,
1727                                int row,
1728                                bool expectedResult,
1729                                const QString& expectedParentName,
1730                                int expectedPos)
1731 {
1732     QVERIFY(sourceIndex.isValid());
1733     QVERIFY(targetIndex.isValid());
1734 
1735     auto groupModel = qobject_cast<GroupModel*>(m_dbWidget->findChild<GroupView*>("groupView")->model());
1736 
1737     QMimeData mimeData;
1738     QByteArray encoded;
1739     QDataStream stream(&encoded, QIODevice::WriteOnly);
1740     Group* group = groupModel->groupFromIndex(sourceIndex);
1741     stream << group->database()->uuid() << group->uuid();
1742     mimeData.setData("application/x-keepassx-group", encoded);
1743 
1744     QCOMPARE(groupModel->dropMimeData(&mimeData, Qt::MoveAction, row, 0, targetIndex), expectedResult);
1745     QCOMPARE(group->parentGroup()->name(), expectedParentName);
1746     QCOMPARE(group->parentGroup()->children().indexOf(group), expectedPos);
1747 }
1748 
clickIndex(const QModelIndex & index,QAbstractItemView * view,Qt::MouseButton button,Qt::KeyboardModifiers stateKey)1749 void TestGui::clickIndex(const QModelIndex& index,
1750                          QAbstractItemView* view,
1751                          Qt::MouseButton button,
1752                          Qt::KeyboardModifiers stateKey)
1753 {
1754     QTest::mouseClick(view->viewport(), button, stateKey, view->visualRect(index).center());
1755 }
1756