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