TestGui.cpp 100 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322
  1. /*
  2. * Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
  3. * Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation, either version 2 or (at your option)
  8. * version 3 of the License.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. */
  18. #include "TestGui.h"
  19. #include "gui/Application.h"
  20. #include <QCheckBox>
  21. #include <QClipboard>
  22. #include <QMimeData>
  23. #include <QPlainTextEdit>
  24. #include <QPushButton>
  25. #include <QRadioButton>
  26. #include <QSignalSpy>
  27. #include <QSpinBox>
  28. #include <QTest>
  29. #include <QToolBar>
  30. #include "config-keepassx-tests.h"
  31. #include "core/PasswordHealth.h"
  32. #include "core/Tools.h"
  33. #include "crypto/Crypto.h"
  34. #include "gui/ApplicationSettingsWidget.h"
  35. #include "gui/CategoryListWidget.h"
  36. #include "gui/CloneDialog.h"
  37. #include "gui/DatabaseTabWidget.h"
  38. #include "gui/EntryPreviewWidget.h"
  39. #include "gui/FileDialog.h"
  40. #include "gui/MessageBox.h"
  41. #include "gui/PasswordGeneratorWidget.h"
  42. #include "gui/PasswordWidget.h"
  43. #include "gui/SearchWidget.h"
  44. #include "gui/TotpDialog.h"
  45. #include "gui/TotpSetupDialog.h"
  46. #include "gui/databasekey/KeyFileEditWidget.h"
  47. #include "gui/databasekey/PasswordEditWidget.h"
  48. #include "gui/dbsettings/DatabaseSettingsDialog.h"
  49. #include "gui/dbsettings/DatabaseSettingsWidgetEncryption.h"
  50. #include "gui/entry/EditEntryWidget.h"
  51. #include "gui/entry/EntryView.h"
  52. #include "gui/group/EditGroupWidget.h"
  53. #include "gui/group/GroupModel.h"
  54. #include "gui/group/GroupView.h"
  55. #include "gui/tag/TagsEdit.h"
  56. #include "gui/wizard/NewDatabaseWizard.h"
  57. #include "keys/FileKey.h"
  58. #define TEST_MODAL_NO_WAIT(TEST_CODE) \
  59. bool dialogFinished = false; \
  60. QTimer::singleShot(0, [&]() { TEST_CODE dialogFinished = true; })
  61. #define TEST_MODAL(TEST_CODE) \
  62. TEST_MODAL_NO_WAIT(TEST_CODE); \
  63. QTRY_VERIFY(dialogFinished)
  64. int main(int argc, char* argv[])
  65. {
  66. #if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
  67. QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
  68. QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
  69. #endif
  70. Application app(argc, argv);
  71. app.setApplicationName("KeePassXC");
  72. app.setApplicationVersion(KEEPASSXC_VERSION);
  73. app.setQuitOnLastWindowClosed(false);
  74. app.setAttribute(Qt::AA_Use96Dpi, true);
  75. app.applyTheme();
  76. QTEST_DISABLE_KEYPAD_NAVIGATION
  77. TestGui tc;
  78. QTEST_SET_MAIN_SOURCE_PATH
  79. return QTest::qExec(&tc, argc, argv);
  80. }
  81. void TestGui::initTestCase()
  82. {
  83. QVERIFY(Crypto::init());
  84. Config::createTempFileInstance();
  85. QLocale::setDefault(QLocale::c());
  86. Application::bootstrap();
  87. m_mainWindow.reset(new MainWindow());
  88. m_tabWidget = m_mainWindow->findChild<DatabaseTabWidget*>("tabWidget");
  89. m_statusBarLabel = m_mainWindow->findChild<QLabel*>("statusBarLabel");
  90. m_mainWindow->show();
  91. m_mainWindow->resize(1024, 768);
  92. }
  93. // Every test starts with resetting config settings and opening the temp database
  94. void TestGui::init()
  95. {
  96. // Reset config to defaults
  97. config()->resetToDefaults();
  98. // Disable autosave so we can test the modified file indicator
  99. config()->set(Config::AutoSaveAfterEveryChange, false);
  100. config()->set(Config::AutoSaveOnExit, false);
  101. // Enable the tray icon so we can test hiding/restoring the windowQByteArray
  102. config()->set(Config::GUI_ShowTrayIcon, true);
  103. // Disable the update check first time alert
  104. config()->set(Config::UpdateCheckMessageShown, true);
  105. // Disable quick unlock
  106. config()->set(Config::Security_QuickUnlock, false);
  107. // Disable atomic saves to prevent transient errors on some platforms
  108. config()->set(Config::UseAtomicSaves, false);
  109. // Disable showing expired entries on unlock
  110. config()->set(Config::GUI_ShowExpiredEntriesOnDatabaseUnlock, false);
  111. // Copy the test database file to the temporary file
  112. auto origFilePath = QDir(KEEPASSX_TEST_DATA_DIR).absoluteFilePath("NewDatabase.kdbx");
  113. QVERIFY(m_dbFile.copyFromFile(origFilePath));
  114. m_dbFileName = QFileInfo(m_dbFile.fileName()).fileName();
  115. m_dbFilePath = m_dbFile.fileName();
  116. // make sure window is activated or focus tests may fail
  117. m_mainWindow->activateWindow();
  118. QApplication::processEvents();
  119. fileDialog()->setNextFileName(m_dbFilePath);
  120. triggerAction("actionDatabaseOpen");
  121. QApplication::processEvents();
  122. m_dbWidget = m_tabWidget->currentDatabaseWidget();
  123. auto* databaseOpenWidget = m_tabWidget->currentDatabaseWidget()->findChild<QWidget*>("databaseOpenWidget");
  124. QVERIFY(databaseOpenWidget);
  125. // editPassword is not QLineEdit anymore but PasswordWidget
  126. auto* editPassword =
  127. databaseOpenWidget->findChild<PasswordWidget*>("editPassword")->findChild<QLineEdit*>("passwordEdit");
  128. QVERIFY(editPassword);
  129. editPassword->setFocus();
  130. QTRY_VERIFY(editPassword->hasFocus());
  131. QTest::keyClicks(editPassword, "a");
  132. QTest::keyClick(editPassword, Qt::Key_Enter);
  133. QTRY_VERIFY(!m_dbWidget->isLocked());
  134. m_db = m_dbWidget->database();
  135. QApplication::processEvents();
  136. }
  137. // Every test ends with closing the temp database without saving
  138. void TestGui::cleanup()
  139. {
  140. if (m_tabWidget->isVisible()) {
  141. // DO NOT save the database
  142. m_db->markAsClean();
  143. MessageBox::setNextAnswer(MessageBox::No);
  144. triggerAction("actionDatabaseClose");
  145. QApplication::processEvents();
  146. MessageBox::setNextAnswer(MessageBox::NoButton);
  147. delete m_dbWidget;
  148. }
  149. }
  150. void TestGui::cleanupTestCase()
  151. {
  152. m_dbFile.remove();
  153. }
  154. void TestGui::testSettingsDefaultTabOrder()
  155. {
  156. // check application settings default tab order
  157. triggerAction("actionSettings");
  158. auto* settingsWidget = m_mainWindow->findChild<ApplicationSettingsWidget*>();
  159. QVERIFY(settingsWidget->isVisible());
  160. QCOMPARE(settingsWidget->findChild<CategoryListWidget*>("categoryList")->currentCategory(), 0);
  161. for (auto* w : settingsWidget->findChildren<QTabWidget*>()) {
  162. if (w->currentIndex() != 0) {
  163. QFAIL("Application settings contain QTabWidgets whose default index is not 0");
  164. }
  165. }
  166. QTest::keyClick(settingsWidget, Qt::Key::Key_Escape);
  167. // check database settings default tab order
  168. triggerAction("actionDatabaseSettings");
  169. auto* dbSettingsWidget = m_mainWindow->findChild<DatabaseSettingsDialog*>();
  170. QVERIFY(dbSettingsWidget->isVisible());
  171. QCOMPARE(dbSettingsWidget->findChild<CategoryListWidget*>("categoryList")->currentCategory(), 0);
  172. for (auto* w : dbSettingsWidget->findChildren<QTabWidget*>()) {
  173. if (w->currentIndex() != 0 && w->objectName() != "encryptionSettingsTabWidget") {
  174. QFAIL("Database settings contain QTabWidgets whose default index is not 0");
  175. }
  176. }
  177. QTest::keyClick(dbSettingsWidget, Qt::Key::Key_Escape);
  178. }
  179. void TestGui::testCreateDatabase()
  180. {
  181. TEST_MODAL_NO_WAIT(
  182. NewDatabaseWizard * wizard; QTRY_VERIFY(wizard = m_tabWidget->findChild<NewDatabaseWizard*>());
  183. QTest::keyClicks(wizard->currentPage()->findChild<QLineEdit*>("databaseName"), "Test Name");
  184. QTest::keyClicks(wizard->currentPage()->findChild<QLineEdit*>("databaseDescription"), "Test Description");
  185. QCOMPARE(wizard->currentId(), 0);
  186. QTest::keyClick(wizard, Qt::Key_Enter);
  187. QCOMPARE(wizard->currentId(), 1);
  188. // Check that basic encryption settings are visible
  189. auto decryptionTimeSlider = wizard->currentPage()->findChild<QSlider*>("decryptionTimeSlider");
  190. auto algorithmComboBox = wizard->currentPage()->findChild<QComboBox*>("algorithmComboBox");
  191. QTRY_VERIFY(decryptionTimeSlider->isVisible());
  192. QVERIFY(!algorithmComboBox->isVisible());
  193. // Set the encryption settings to the advanced view
  194. auto encryptionSettings = wizard->currentPage()->findChild<QTabWidget*>("encryptionSettingsTabWidget");
  195. auto advancedTab = encryptionSettings->findChild<QWidget*>("advancedTab");
  196. encryptionSettings->setCurrentWidget(advancedTab);
  197. QTRY_VERIFY(!decryptionTimeSlider->isVisible());
  198. QVERIFY(algorithmComboBox->isVisible());
  199. auto rounds = wizard->currentPage()->findChild<QSpinBox*>("transformRoundsSpinBox");
  200. QVERIFY(rounds);
  201. QVERIFY(rounds->isVisible());
  202. QTest::mouseClick(rounds, Qt::MouseButton::LeftButton);
  203. QTest::keyClick(rounds, Qt::Key_A, Qt::ControlModifier);
  204. QTest::keyClicks(rounds, "2");
  205. QTest::keyClick(rounds, Qt::Key_Tab);
  206. QTest::keyClick(rounds, Qt::Key_Tab);
  207. auto memory = wizard->currentPage()->findChild<QSpinBox*>("memorySpinBox");
  208. QVERIFY(memory);
  209. QVERIFY(memory->isVisible());
  210. QTest::mouseClick(memory, Qt::MouseButton::LeftButton);
  211. QTest::keyClick(memory, Qt::Key_A, Qt::ControlModifier);
  212. QTest::keyClicks(memory, "50");
  213. QTest::keyClick(memory, Qt::Key_Tab);
  214. auto parallelism = wizard->currentPage()->findChild<QSpinBox*>("parallelismSpinBox");
  215. QVERIFY(parallelism);
  216. QVERIFY(parallelism->isVisible());
  217. QTest::mouseClick(parallelism, Qt::MouseButton::LeftButton);
  218. QTest::keyClick(parallelism, Qt::Key_A, Qt::ControlModifier);
  219. QTest::keyClicks(parallelism, "1");
  220. QTest::keyClick(parallelism, Qt::Key_Enter);
  221. QCOMPARE(wizard->currentId(), 2);
  222. // enter password
  223. auto* passwordWidget = wizard->currentPage()->findChild<PasswordEditWidget*>();
  224. QCOMPARE(passwordWidget->visiblePage(), KeyFileEditWidget::Page::Edit);
  225. auto* passwordEdit =
  226. passwordWidget->findChild<PasswordWidget*>("enterPasswordEdit")->findChild<QLineEdit*>("passwordEdit");
  227. auto* passwordRepeatEdit =
  228. passwordWidget->findChild<PasswordWidget*>("repeatPasswordEdit")->findChild<QLineEdit*>("passwordEdit");
  229. QTRY_VERIFY(passwordEdit->isVisible());
  230. QTRY_VERIFY(passwordEdit->hasFocus());
  231. QTest::keyClicks(passwordEdit, "test");
  232. QTest::keyClick(passwordEdit, Qt::Key::Key_Tab);
  233. QTest::keyClicks(passwordRepeatEdit, "test");
  234. // add key file
  235. auto* additionalOptionsButton = wizard->currentPage()->findChild<QPushButton*>("additionalKeyOptionsToggle");
  236. auto* keyFileWidget = wizard->currentPage()->findChild<KeyFileEditWidget*>();
  237. QVERIFY(additionalOptionsButton->isVisible());
  238. QTest::mouseClick(additionalOptionsButton, Qt::MouseButton::LeftButton);
  239. QTRY_VERIFY(keyFileWidget->isVisible());
  240. QTRY_VERIFY(!additionalOptionsButton->isVisible());
  241. QCOMPARE(passwordWidget->visiblePage(), KeyFileEditWidget::Page::Edit);
  242. QTest::mouseClick(keyFileWidget->findChild<QPushButton*>("addButton"), Qt::MouseButton::LeftButton);
  243. auto* fileEdit = keyFileWidget->findChild<QLineEdit*>("keyFileLineEdit");
  244. QTRY_VERIFY(fileEdit);
  245. QTRY_VERIFY(fileEdit->isVisible());
  246. fileDialog()->setNextFileName(QString("%1/%2").arg(QString(KEEPASSX_TEST_DATA_DIR), "FileKeyHashed.key"));
  247. QTest::keyClick(keyFileWidget->findChild<QPushButton*>("addButton"), Qt::Key::Key_Enter);
  248. QVERIFY(fileEdit->hasFocus());
  249. auto* browseButton = keyFileWidget->findChild<QPushButton*>("browseKeyFileButton");
  250. QTest::keyClick(browseButton, Qt::Key::Key_Enter);
  251. QCOMPARE(fileEdit->text(), QString("%1/%2").arg(QString(KEEPASSX_TEST_DATA_DIR), "FileKeyHashed.key"));
  252. // save database to temporary file
  253. TemporaryFile tmpFile;
  254. QVERIFY(tmpFile.open());
  255. tmpFile.close();
  256. fileDialog()->setNextFileName(tmpFile.fileName());
  257. // click Continue on the warning due to weak password
  258. MessageBox::setNextAnswer(MessageBox::ContinueWithWeakPass);
  259. QTest::keyClick(fileEdit, Qt::Key::Key_Enter);
  260. tmpFile.remove(););
  261. triggerAction("actionDatabaseNew");
  262. QCOMPARE(m_tabWidget->count(), 2);
  263. checkStatusBarText("0 Ent");
  264. // there is a new empty db
  265. m_db = m_tabWidget->currentDatabaseWidget()->database();
  266. QCOMPARE(m_db->rootGroup()->children().size(), 0);
  267. // check meta data
  268. QCOMPARE(m_db->metadata()->name(), QString("Test Name"));
  269. QCOMPARE(m_db->metadata()->description(), QString("Test Description"));
  270. // check key and encryption
  271. QCOMPARE(m_db->key()->keys().size(), 2);
  272. QCOMPARE(m_db->kdf()->rounds(), 2);
  273. QCOMPARE(m_db->kdf()->uuid(), KeePass2::KDF_ARGON2D);
  274. QCOMPARE(m_db->cipher(), KeePass2::CIPHER_AES256);
  275. auto compositeKey = QSharedPointer<CompositeKey>::create();
  276. compositeKey->addKey(QSharedPointer<PasswordKey>::create("test"));
  277. auto fileKey = QSharedPointer<FileKey>::create();
  278. fileKey->load(QString("%1/%2").arg(QString(KEEPASSX_TEST_DATA_DIR), "FileKeyHashed.key"));
  279. compositeKey->addKey(fileKey);
  280. QCOMPARE(m_db->key()->rawKey(), compositeKey->rawKey());
  281. checkStatusBarText("0 Ent");
  282. // Test the switching to other DB tab
  283. m_tabWidget->setCurrentIndex(0);
  284. checkStatusBarText("1 Ent");
  285. m_tabWidget->setCurrentIndex(1);
  286. checkStatusBarText("0 Ent");
  287. // close the new database
  288. MessageBox::setNextAnswer(MessageBox::No);
  289. triggerAction("actionDatabaseClose");
  290. // Wait for dialog to terminate
  291. QTRY_VERIFY(dialogFinished);
  292. }
  293. void TestGui::testMergeDatabase()
  294. {
  295. // It is safe to ignore the warning this line produces
  296. QSignalSpy dbMergeSpy(m_dbWidget.data(), SIGNAL(databaseMerged(QSharedPointer<Database>)));
  297. QApplication::processEvents();
  298. // set file to merge from
  299. fileDialog()->setNextFileName(QString(KEEPASSX_TEST_DATA_DIR).append("/MergeDatabase.kdbx"));
  300. triggerAction("actionDatabaseMerge");
  301. QTRY_COMPARE(QApplication::focusWidget()->objectName(), QString("passwordEdit"));
  302. auto* editPasswordMerge = QApplication::focusWidget();
  303. QVERIFY(editPasswordMerge->isVisible());
  304. QTest::keyClicks(editPasswordMerge, "a");
  305. QTest::keyClick(editPasswordMerge, Qt::Key_Enter);
  306. QTRY_COMPARE(dbMergeSpy.count(), 1);
  307. QTRY_VERIFY(m_tabWidget->tabText(m_tabWidget->currentIndex()).contains("*"));
  308. m_db = m_tabWidget->currentDatabaseWidget()->database();
  309. // there are seven child groups of the root group
  310. QCOMPARE(m_db->rootGroup()->children().size(), 7);
  311. // the merged group should contain an entry
  312. QCOMPARE(m_db->rootGroup()->children().at(6)->entries().size(), 1);
  313. // the General group contains one entry merged from the other db
  314. QCOMPARE(m_db->rootGroup()->findChildByName("General")->entries().size(), 1);
  315. }
  316. void TestGui::testAutoreloadDatabase()
  317. {
  318. config()->set(Config::AutoReloadOnChange, false);
  319. // Test accepting new file in autoreload
  320. MessageBox::setNextAnswer(MessageBox::Yes);
  321. // Overwrite the current database with the temp data
  322. QVERIFY(m_dbFile.copyFromFile(QString(KEEPASSX_TEST_DATA_DIR).append("/MergeDatabase.kdbx")));
  323. QTRY_VERIFY(m_db != m_dbWidget->database());
  324. m_db = m_dbWidget->database();
  325. // the General group contains one entry from the new db data
  326. QCOMPARE(m_db->rootGroup()->findChildByName("General")->entries().size(), 1);
  327. QVERIFY(!m_tabWidget->tabText(m_tabWidget->currentIndex()).endsWith("*"));
  328. // Reset the state
  329. cleanup();
  330. init();
  331. config()->set(Config::AutoReloadOnChange, false);
  332. // Test rejecting new file in autoreload
  333. MessageBox::setNextAnswer(MessageBox::No);
  334. // Overwrite the current database with the temp data
  335. QVERIFY(m_dbFile.copyFromFile(QString(KEEPASSX_TEST_DATA_DIR).append("/MergeDatabase.kdbx")));
  336. // Ensure the merge did not take place
  337. QCOMPARE(m_db->rootGroup()->findChildByName("General")->entries().size(), 0);
  338. QTRY_VERIFY(m_tabWidget->tabText(m_tabWidget->currentIndex()).endsWith("*"));
  339. // Reset the state
  340. cleanup();
  341. init();
  342. // Test accepting a merge of edits into autoreload
  343. // Turn on autoload so we only get one messagebox (for the merge)
  344. config()->set(Config::AutoReloadOnChange, true);
  345. // Modify some entries
  346. testEditEntry();
  347. // This is saying yes to merging the entries
  348. MessageBox::setNextAnswer(MessageBox::Merge);
  349. // Overwrite the current database with the temp data
  350. QVERIFY(m_dbFile.copyFromFile(QString(KEEPASSX_TEST_DATA_DIR).append("/MergeDatabase.kdbx")));
  351. QTRY_VERIFY(m_db != m_dbWidget->database());
  352. m_db = m_dbWidget->database();
  353. QCOMPARE(m_db->rootGroup()->findChildByName("General")->entries().size(), 1);
  354. QTRY_VERIFY(m_tabWidget->tabText(m_tabWidget->currentIndex()).endsWith("*"));
  355. }
  356. void TestGui::testTabs()
  357. {
  358. QCOMPARE(m_tabWidget->count(), 1);
  359. QCOMPARE(m_tabWidget->tabText(m_tabWidget->currentIndex()), m_dbFileName);
  360. }
  361. void TestGui::testEditEntry()
  362. {
  363. auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
  364. auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
  365. entryView->setFocus();
  366. QVERIFY(entryView->hasFocus());
  367. // Select the first entry in the database
  368. QModelIndex entryItem = entryView->model()->index(0, 1);
  369. Entry* entry = entryView->entryFromIndex(entryItem);
  370. clickIndex(entryItem, entryView, Qt::LeftButton);
  371. // Confirm the edit action button is enabled
  372. auto* entryEditAction = m_mainWindow->findChild<QAction*>("actionEntryEdit");
  373. QVERIFY(entryEditAction->isEnabled());
  374. QWidget* entryEditWidget = toolBar->widgetForAction(entryEditAction);
  375. QVERIFY(entryEditWidget->isVisible());
  376. QVERIFY(entryEditWidget->isEnabled());
  377. // Record current history count
  378. int editCount = entry->historyItems().size();
  379. // Edit the first entry ("Sample Entry")
  380. QTest::mouseClick(entryEditWidget, Qt::LeftButton);
  381. QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditEntryMode);
  382. auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
  383. auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
  384. QTest::keyClicks(titleEdit, "_test");
  385. auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
  386. QVERIFY(editEntryWidgetButtonBox);
  387. auto* okButton = editEntryWidgetButtonBox->button(QDialogButtonBox::Ok);
  388. QVERIFY(okButton);
  389. auto* applyButton = editEntryWidgetButtonBox->button(QDialogButtonBox::Apply);
  390. QVERIFY(applyButton);
  391. // Apply the edit
  392. QTRY_VERIFY(applyButton->isEnabled());
  393. QTest::mouseClick(applyButton, Qt::LeftButton);
  394. QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditEntryMode);
  395. QCOMPARE(entry->title(), QString("Sample Entry_test"));
  396. QCOMPARE(entry->historyItems().size(), ++editCount);
  397. QVERIFY(!applyButton->isEnabled());
  398. // Test viewing entry history
  399. auto historyView = editEntryWidget->findChild<QTreeView*>("historyView");
  400. auto showButton = editEntryWidget->findChild<QPushButton*>("showButton");
  401. QVERIFY(historyView);
  402. editEntryWidget->switchToPage(EditEntryWidget::Page::History);
  403. QApplication::processEvents();
  404. QVERIFY(historyView->isVisible());
  405. QVERIFY(!showButton->isEnabled());
  406. // Select the second row in the history view
  407. historyView->setCurrentIndex(historyView->model()->index(1, 0));
  408. QVERIFY(showButton->isEnabled());
  409. QTest::mouseClick(showButton, Qt::LeftButton);
  410. // Verify that the entry history widget is shown
  411. auto entryHistoryWidget = m_dbWidget->findChild<QWidget*>("editEntryHistoryWidget");
  412. QVERIFY(entryHistoryWidget);
  413. QVERIFY(entryHistoryWidget->isVisible());
  414. QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditEntryMode);
  415. QTest::keyClick(entryHistoryWidget, Qt::Key_Escape);
  416. QVERIFY(historyView->isVisible());
  417. // Test the "known bad" checkbox
  418. editEntryWidget->switchToPage(EditEntryWidget::Page::Advanced);
  419. auto excludeReportsCheckBox = editEntryWidget->findChild<QCheckBox*>("excludeReportsCheckBox");
  420. QVERIFY(excludeReportsCheckBox);
  421. QCOMPARE(excludeReportsCheckBox->isChecked(), false);
  422. excludeReportsCheckBox->setChecked(true);
  423. QTest::mouseClick(applyButton, Qt::LeftButton);
  424. QCOMPARE(entry->historyItems().size(), ++editCount);
  425. QVERIFY(entry->excludeFromReports());
  426. // Test tags
  427. auto* tags = editEntryWidget->findChild<TagsEdit*>("tagsList");
  428. QTest::keyClicks(tags, "_tag1");
  429. QTest::keyClick(tags, Qt::Key_Return);
  430. QCOMPARE(tags->tags().last(), QString("_tag1"));
  431. QTest::keyClicks(tags, "tag 2"); // adds another tag
  432. QTest::keyClick(tags, Qt::Key_Return);
  433. QCOMPARE(tags->tags().last(), QString("tag 2"));
  434. QTest::keyClick(tags, Qt::Key_Backspace); // Back into editing last tag
  435. QTest::keyClicks(tags, "_is!awesome");
  436. QTest::keyClick(tags, Qt::Key_Return);
  437. QCOMPARE(tags->tags().last(), QString("tag 2_is!awesome"));
  438. // Test entry colors (simulate choosing a color)
  439. editEntryWidget->switchToPage(EditEntryWidget::Page::Advanced);
  440. auto fgColor = QString("#FF0000");
  441. auto bgColor = QString("#0000FF");
  442. // Set foreground color
  443. auto colorButton = editEntryWidget->findChild<QPushButton*>("fgColorButton");
  444. auto colorCheckBox = editEntryWidget->findChild<QCheckBox*>("fgColorCheckBox");
  445. colorButton->setProperty("color", fgColor);
  446. colorCheckBox->setChecked(true);
  447. // Set background color
  448. colorButton = editEntryWidget->findChild<QPushButton*>("bgColorButton");
  449. colorCheckBox = editEntryWidget->findChild<QCheckBox*>("bgColorCheckBox");
  450. colorButton->setProperty("color", bgColor);
  451. colorCheckBox->setChecked(true);
  452. QTest::mouseClick(applyButton, Qt::LeftButton);
  453. QCOMPARE(entry->historyItems().size(), ++editCount);
  454. // Test protected attributes
  455. editEntryWidget->switchToPage(EditEntryWidget::Page::Advanced);
  456. auto* attrTextEdit = editEntryWidget->findChild<QPlainTextEdit*>("attributesEdit");
  457. QTest::mouseClick(editEntryWidget->findChild<QAbstractButton*>("addAttributeButton"), Qt::LeftButton);
  458. QString attrText = "TEST TEXT";
  459. QTest::keyClicks(attrTextEdit, attrText);
  460. QCOMPARE(attrTextEdit->toPlainText(), attrText);
  461. QTest::mouseClick(editEntryWidget->findChild<QAbstractButton*>("protectAttributeButton"), Qt::LeftButton);
  462. QVERIFY(attrTextEdit->toPlainText().contains("PROTECTED"));
  463. QTest::mouseClick(editEntryWidget->findChild<QAbstractButton*>("revealAttributeButton"), Qt::LeftButton);
  464. QCOMPARE(attrTextEdit->toPlainText(), attrText);
  465. editEntryWidget->switchToPage(EditEntryWidget::Page::Main);
  466. // Save the edit (press OK)
  467. QTest::mouseClick(okButton, Qt::LeftButton);
  468. QApplication::processEvents();
  469. // Confirm edit was made
  470. QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
  471. QCOMPARE(entry->title(), QString("Sample Entry_test"));
  472. QCOMPARE(entry->foregroundColor().toUpper(), fgColor.toUpper());
  473. QCOMPARE(entryItem.data(Qt::ForegroundRole), QVariant(fgColor));
  474. QCOMPARE(entry->backgroundColor().toUpper(), bgColor.toUpper());
  475. QCOMPARE(entryItem.data(Qt::BackgroundRole), QVariant(bgColor));
  476. QCOMPARE(entry->historyItems().size(), ++editCount);
  477. // Confirm modified indicator is showing
  478. QTRY_COMPARE(m_tabWidget->tabText(m_tabWidget->currentIndex()), QString("%1*").arg(m_dbFileName));
  479. // Test copy & paste newline sanitization
  480. QTest::mouseClick(entryEditWidget, Qt::LeftButton);
  481. okButton = editEntryWidgetButtonBox->button(QDialogButtonBox::Ok);
  482. QVERIFY(okButton);
  483. QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditEntryMode);
  484. titleEdit->setText("multiline\ntitle");
  485. editEntryWidget->findChild<QComboBox*>("usernameComboBox")->lineEdit()->setText("multiline\nusername");
  486. editEntryWidget->findChild<PasswordWidget*>("passwordEdit")->setText("multiline\npassword");
  487. editEntryWidget->findChild<QLineEdit*>("urlEdit")->setText("multiline\nurl");
  488. QTest::mouseClick(okButton, Qt::LeftButton);
  489. QCOMPARE(entry->title(), QString("multiline title"));
  490. QCOMPARE(entry->username(), QString("multiline username"));
  491. // here we keep newlines, so users can't lock themselves out accidentally
  492. QCOMPARE(entry->password(), QString("multiline\npassword"));
  493. QCOMPARE(entry->url(), QString("multiline url"));
  494. }
  495. void TestGui::testSearchEditEntry()
  496. {
  497. // Regression test for Issue #1447 -- Uses example from issue description
  498. // Find buttons for group creation
  499. auto* editGroupWidget = m_dbWidget->findChild<EditGroupWidget*>("editGroupWidget");
  500. auto* nameEdit = editGroupWidget->findChild<QLineEdit*>("editName");
  501. auto* editGroupWidgetButtonBox = editGroupWidget->findChild<QDialogButtonBox*>("buttonBox");
  502. // Add groups "Good" and "Bad"
  503. m_dbWidget->createGroup();
  504. QTest::keyClicks(nameEdit, "Good");
  505. QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
  506. m_dbWidget->groupView()->setCurrentGroup(m_db->rootGroup()); // Makes "Good" and "Bad" on the same level
  507. m_dbWidget->createGroup();
  508. QTest::keyClicks(nameEdit, "Bad");
  509. QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
  510. m_dbWidget->groupView()->setCurrentGroup(m_db->rootGroup());
  511. // Find buttons for entry creation
  512. auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
  513. QWidget* entryNewWidget = toolBar->widgetForAction(m_mainWindow->findChild<QAction*>("actionEntryNew"));
  514. auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
  515. auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
  516. auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
  517. // Create "Doggy" in "Good"
  518. Group* goodGroup = m_dbWidget->currentGroup()->findChildByName(QString("Good"));
  519. m_dbWidget->groupView()->setCurrentGroup(goodGroup);
  520. QTest::mouseClick(entryNewWidget, Qt::LeftButton);
  521. QTest::keyClicks(titleEdit, "Doggy");
  522. QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
  523. // Select "Bad" group in groupView
  524. Group* badGroup = m_db->rootGroup()->findChildByName(QString("Bad"));
  525. m_dbWidget->groupView()->setCurrentGroup(badGroup);
  526. // Search for "Doggy" entry
  527. auto* searchWidget = toolBar->findChild<SearchWidget*>("SearchWidget");
  528. auto* searchTextEdit = searchWidget->findChild<QLineEdit*>("searchEdit");
  529. QTest::mouseClick(searchTextEdit, Qt::LeftButton);
  530. QTest::keyClicks(searchTextEdit, "Doggy");
  531. QTRY_VERIFY(m_dbWidget->isSearchActive());
  532. // Goto "Doggy"'s edit view
  533. QTest::keyClick(searchTextEdit, Qt::Key_Return);
  534. QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditEntryMode);
  535. // Check the path in header is "parent-group > entry"
  536. QCOMPARE(m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget")->findChild<QLabel*>("headerLabel")->text(),
  537. QStringLiteral("Good \u2022 Doggy \u2022 Edit entry"));
  538. }
  539. void TestGui::testAddEntry()
  540. {
  541. auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
  542. auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
  543. // Given the status bar label with initial number of entries.
  544. checkStatusBarText("1 Ent");
  545. // Find the new entry action
  546. auto* entryNewAction = m_mainWindow->findChild<QAction*>("actionEntryNew");
  547. QVERIFY(entryNewAction->isEnabled());
  548. // Find the button associated with the new entry action
  549. QWidget* entryNewWidget = toolBar->widgetForAction(entryNewAction);
  550. QVERIFY(entryNewWidget->isVisible());
  551. QVERIFY(entryNewWidget->isEnabled());
  552. // Click the new entry button and check that we enter edit mode
  553. QTest::mouseClick(entryNewWidget, Qt::LeftButton);
  554. QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditEntryMode);
  555. // Add entry "test" and confirm added
  556. auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
  557. auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
  558. QTest::keyClicks(titleEdit, "test");
  559. auto* usernameComboBox = editEntryWidget->findChild<QComboBox*>("usernameComboBox");
  560. QVERIFY(usernameComboBox);
  561. QTest::mouseClick(usernameComboBox, Qt::LeftButton);
  562. QTest::keyClicks(usernameComboBox, "AutocompletionUsername");
  563. auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
  564. QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
  565. QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
  566. QModelIndex item = entryView->model()->index(1, 1);
  567. Entry* entry = entryView->entryFromIndex(item);
  568. QCOMPARE(entry->title(), QString("test"));
  569. QCOMPARE(entry->username(), QString("AutocompletionUsername"));
  570. QCOMPARE(entry->historyItems().size(), 0);
  571. m_db->updateCommonUsernames();
  572. // Then the status bar label should be updated with incremented number of entries.
  573. checkStatusBarText("2 Ent");
  574. // Add entry "something 2"
  575. QTest::mouseClick(entryNewWidget, Qt::LeftButton);
  576. QTest::keyClicks(titleEdit, "something 2");
  577. QTest::mouseClick(usernameComboBox, Qt::LeftButton);
  578. QTest::keyClicks(usernameComboBox, "Auto");
  579. QTest::keyPress(usernameComboBox, Qt::Key_Right);
  580. auto* passwordEdit =
  581. editEntryWidget->findChild<PasswordWidget*>("passwordEdit")->findChild<QLineEdit*>("passwordEdit");
  582. QTest::keyClicks(passwordEdit, "something 2");
  583. QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
  584. QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
  585. item = entryView->model()->index(1, 1);
  586. entry = entryView->entryFromIndex(item);
  587. QCOMPARE(entry->title(), QString("something 2"));
  588. QCOMPARE(entry->username(), QString("AutocompletionUsername"));
  589. QCOMPARE(entry->historyItems().size(), 0);
  590. // Add entry "something 5" but click cancel button (does NOT add entry)
  591. QTest::mouseClick(entryNewWidget, Qt::LeftButton);
  592. QTest::keyClicks(titleEdit, "something 5");
  593. MessageBox::setNextAnswer(MessageBox::Discard);
  594. QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Cancel), Qt::LeftButton);
  595. QApplication::processEvents();
  596. // Confirm no changed entry count
  597. QTRY_COMPARE(entryView->model()->rowCount(), 3);
  598. }
  599. void TestGui::testPasswordEntryEntropy_data()
  600. {
  601. QTest::addColumn<QString>("password");
  602. QTest::addColumn<QString>("expectedStrengthLabel");
  603. QTest::newRow("Empty password") << "" << "Password Quality: Poor";
  604. QTest::newRow("Well-known password") << "hello" << "Password Quality: Poor";
  605. QTest::newRow("Password composed of well-known words.") << "helloworld" << "Password Quality: Poor";
  606. QTest::newRow("Password composed of well-known words with number.") << "password1" << "Password Quality: Poor";
  607. QTest::newRow("Password out of small character space.") << "D0g.................." << "Password Quality: Poor";
  608. QTest::newRow("XKCD, easy substitutions.") << "Tr0ub4dour&3" << "Password Quality: Poor";
  609. QTest::newRow("XKCD, word generator.") << "correcthorsebatterystaple" << "Password Quality: Weak";
  610. QTest::newRow("Random characters, medium length.") << "YQC3kbXbjC652dTDH" << "Password Quality: Good";
  611. QTest::newRow("Random characters, long.") << "Bs5ZFfthWzR8DGFEjaCM6bGqhmCT4km" << "Password Quality: Excellent";
  612. QTest::newRow("Long password using Zxcvbn chunk estimation")
  613. << "quintet-tamper-kinswoman-humility-vengeful-haven-tastiness-aspire-widget-ipad-cussed-reaffirm-ladylike-"
  614. "ashamed-anatomy-daybed-jam-swear-strudel-neatness-stalemate-unbundle-flavored-relation-emergency-underrate-"
  615. "registry-getting-award-unveiled-unshaken-stagnate-cartridge-magnitude-ointment-hardener-enforced-scrubbed-"
  616. "radial-fiddling-envelope-unpaved-moisture-unused-crawlers-quartered-crushed-kangaroo-tiptop-doily"
  617. << "Password Quality: Excellent";
  618. QTest::newRow("Longer password above Zxcvbn threshold")
  619. << "quintet-tamper-kinswoman-humility-vengeful-haven-tastiness-aspire-widget-ipad-cussed-reaffirm-ladylike-"
  620. "ashamed-anatomy-daybed-jam-swear-strudel-neatness-stalemate-unbundle-flavored-relation-emergency-underrate-"
  621. "registry-getting-award-unveiled-unshaken-stagnate-cartridge-magnitude-ointment-hardener-enforced-scrubbed-"
  622. "radial-fiddling-envelope-unpaved-moisture-unused-crawlers-quartered-crushed-kangaroo-tiptop-doily-hefty-"
  623. "untie-fidgeting-radiance-twilight-freebase-sulphuric-parrot-decree-monotype-nautical-pout-sip-geometric-"
  624. "crunching-deviancy-festival-hacking-rage-unify-coronary-zigzagged-dwindle-possum-lilly-exhume-daringly-"
  625. "barbell-rage-animate-lapel-emporium-renounce-justifier-relieving-gauze-arrive-alive-collected-immobile-"
  626. "unleash-snowman-gift-expansion-marbles-requisite-excusable-flatness-displace-caloric-sensuous-moustache-"
  627. "sensuous-capillary-aversion-contents-cadet-giggly-amenity-peddling-spotting-drier-mooned-rudder-peroxide-"
  628. "posting-oppressor-scrabble-scorer-whomever-paprika-slapstick-said-spectacle-capture-debate-attire-emcee-"
  629. "unfocused-sympathy-doily-election-ambulance-polish-subtype-grumbling-neon-stooge-reanalyze-rockfish-"
  630. "disparate-decorated-washroom-threefold-muzzle-buckwheat-kerosene-swell-why-reprocess-correct-shady-"
  631. "impatient-slit-banshee-scrubbed-dreadful-unlocking-urologist-hurried-citable-fragment-septic-lapped-"
  632. "prankish-phantom-unpaved-moisture-unused-crawlers-quartered-crushed-kangaroo-lapel-emporium-renounce"
  633. << "Password Quality: Excellent";
  634. }
  635. void TestGui::testPasswordEntryEntropy()
  636. {
  637. auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
  638. // Find the new entry action
  639. auto* entryNewAction = m_mainWindow->findChild<QAction*>("actionEntryNew");
  640. QVERIFY(entryNewAction->isEnabled());
  641. // Find the button associated with the new entry action
  642. QWidget* entryNewWidget = toolBar->widgetForAction(entryNewAction);
  643. QVERIFY(entryNewWidget->isVisible());
  644. QVERIFY(entryNewWidget->isEnabled());
  645. // Click the new entry button and check that we enter edit mode
  646. QTest::mouseClick(entryNewWidget, Qt::LeftButton);
  647. QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditEntryMode);
  648. // Add entry "test" and confirm added
  649. auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
  650. auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
  651. QTest::keyClicks(titleEdit, "test");
  652. // Open the password generator
  653. auto* passwordEdit =
  654. editEntryWidget->findChild<PasswordWidget*>("passwordEdit")->findChild<QLineEdit*>("passwordEdit");
  655. QVERIFY(passwordEdit);
  656. QTest::mouseClick(passwordEdit, Qt::LeftButton);
  657. #ifdef Q_OS_MAC
  658. QTest::keyClick(passwordEdit, Qt::Key_G, Qt::MetaModifier);
  659. #else
  660. QTest::keyClick(passwordEdit, Qt::Key_G, Qt::ControlModifier);
  661. #endif
  662. TEST_MODAL(
  663. PasswordGeneratorWidget * pwGeneratorWidget;
  664. QTRY_VERIFY(pwGeneratorWidget = m_dbWidget->findChild<PasswordGeneratorWidget*>());
  665. // Type in some password
  666. auto* generatedPassword =
  667. pwGeneratorWidget->findChild<PasswordWidget*>("editNewPassword")->findChild<QLineEdit*>("passwordEdit");
  668. auto* entropyLabel = pwGeneratorWidget->findChild<QLabel*>("entropyLabel");
  669. auto* strengthLabel = pwGeneratorWidget->findChild<QLabel*>("strengthLabel");
  670. auto* passwordLengthLabel = pwGeneratorWidget->findChild<QLabel*>("passwordLengthLabel");
  671. QFETCH(QString, password);
  672. QFETCH(QString, expectedStrengthLabel);
  673. // Dynamically calculate entropy due to variances with zxcvbn wordlists
  674. PasswordHealth health(password);
  675. auto expectedEntropy = QString("Entropy: %1 bit").arg(QString::number(health.entropy(), 'f', 2));
  676. auto expectedPasswordLength = QString("Characters: %1").arg(QString::number(password.length()));
  677. generatedPassword->setText(password);
  678. QCOMPARE(entropyLabel->text(), expectedEntropy);
  679. QCOMPARE(strengthLabel->text(), expectedStrengthLabel);
  680. QCOMPARE(passwordLengthLabel->text(), expectedPasswordLength);
  681. QTest::mouseClick(generatedPassword, Qt::LeftButton);
  682. QTest::keyClick(generatedPassword, Qt::Key_Escape););
  683. }
  684. void TestGui::testDicewareEntryEntropy()
  685. {
  686. auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
  687. // Find the new entry action
  688. auto* entryNewAction = m_mainWindow->findChild<QAction*>("actionEntryNew");
  689. QVERIFY(entryNewAction->isEnabled());
  690. // Find the button associated with the new entry action
  691. QWidget* entryNewWidget = toolBar->widgetForAction(entryNewAction);
  692. QVERIFY(entryNewWidget->isVisible());
  693. QVERIFY(entryNewWidget->isEnabled());
  694. // Click the new entry button and check that we enter edit mode
  695. QTest::mouseClick(entryNewWidget, Qt::LeftButton);
  696. QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditEntryMode);
  697. // Add entry "test" and confirm added
  698. auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
  699. auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
  700. QTest::keyClicks(titleEdit, "test");
  701. // Open the password generator
  702. auto* passwordEdit = editEntryWidget->findChild<PasswordWidget*>()->findChild<QLineEdit*>("passwordEdit");
  703. QVERIFY(passwordEdit);
  704. QTest::mouseClick(passwordEdit, Qt::LeftButton);
  705. #ifdef Q_OS_MAC
  706. QTest::keyClick(passwordEdit, Qt::Key_G, Qt::MetaModifier);
  707. #else
  708. QTest::keyClick(passwordEdit, Qt::Key_G, Qt::ControlModifier);
  709. #endif
  710. TEST_MODAL(
  711. PasswordGeneratorWidget * pwGeneratorWidget;
  712. QTRY_VERIFY(pwGeneratorWidget = m_dbWidget->findChild<PasswordGeneratorWidget*>());
  713. // Select Diceware
  714. auto* generatedPassword =
  715. pwGeneratorWidget->findChild<PasswordWidget*>("editNewPassword")->findChild<QLineEdit*>("passwordEdit");
  716. auto* tabWidget = pwGeneratorWidget->findChild<QTabWidget*>("tabWidget");
  717. auto* dicewareWidget = pwGeneratorWidget->findChild<QWidget*>("dicewareWidget");
  718. tabWidget->setCurrentWidget(dicewareWidget);
  719. auto* comboBoxWordList = dicewareWidget->findChild<QComboBox*>("comboBoxWordList");
  720. comboBoxWordList->setCurrentText("eff_large.wordlist");
  721. auto* spinBoxWordCount = dicewareWidget->findChild<QSpinBox*>("spinBoxWordCount");
  722. spinBoxWordCount->setValue(6);
  723. // Confirm a password was generated
  724. QVERIFY(!pwGeneratorWidget->getGeneratedPassword().isEmpty());
  725. // Verify entropy and strength
  726. auto* entropyLabel = pwGeneratorWidget->findChild<QLabel*>("entropyLabel");
  727. auto* strengthLabel = pwGeneratorWidget->findChild<QLabel*>("strengthLabel");
  728. auto* wordLengthLabel = pwGeneratorWidget->findChild<QLabel*>("passwordLengthLabel");
  729. QTRY_COMPARE_WITH_TIMEOUT(entropyLabel->text(), QString("Entropy: 77.54 bit"), 200);
  730. QCOMPARE(strengthLabel->text(), QString("Password Quality: Good"));
  731. QCOMPARE(wordLengthLabel->text(),
  732. QString("Characters: %1").arg(QString::number(pwGeneratorWidget->getGeneratedPassword().length())));
  733. QTest::mouseClick(generatedPassword, Qt::LeftButton);
  734. QTest::keyClick(generatedPassword, Qt::Key_Escape););
  735. }
  736. void TestGui::testTotp()
  737. {
  738. auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
  739. auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
  740. QCOMPARE(entryView->model()->rowCount(), 1);
  741. QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
  742. QModelIndex item = entryView->model()->index(0, 1);
  743. Entry* entry = entryView->entryFromIndex(item);
  744. clickIndex(item, entryView, Qt::LeftButton);
  745. triggerAction("actionEntrySetupTotp");
  746. auto* setupTotpDialog = m_dbWidget->findChild<TotpSetupDialog*>("TotpSetupDialog");
  747. QApplication::processEvents();
  748. QString exampleSeed = "gezd gnbvgY 3tqojqGEZdgnb vgy3tqoJq===";
  749. QString expectedFinalSeed = exampleSeed.toUpper().remove(" ").remove("=");
  750. auto* seedEdit = setupTotpDialog->findChild<QLineEdit*>("seedEdit");
  751. seedEdit->setText("");
  752. QTest::keyClicks(seedEdit, exampleSeed);
  753. auto* setupTotpButtonBox = setupTotpDialog->findChild<QDialogButtonBox*>("buttonBox");
  754. QTest::mouseClick(setupTotpButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
  755. QTRY_VERIFY(!setupTotpDialog->isVisible());
  756. // Make sure the entryView is selected and active
  757. entryView->activateWindow();
  758. QApplication::processEvents();
  759. QTRY_VERIFY(entryView->hasFocus());
  760. auto* entryEditAction = m_mainWindow->findChild<QAction*>("actionEntryEdit");
  761. QWidget* entryEditWidget = toolBar->widgetForAction(entryEditAction);
  762. QVERIFY(entryEditWidget->isVisible());
  763. QVERIFY(entryEditWidget->isEnabled());
  764. QTest::mouseClick(entryEditWidget, Qt::LeftButton);
  765. QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditEntryMode);
  766. auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
  767. editEntryWidget->switchToPage(EditEntryWidget::Page::Advanced);
  768. auto* attrTextEdit = editEntryWidget->findChild<QPlainTextEdit*>("attributesEdit");
  769. QTest::mouseClick(editEntryWidget->findChild<QAbstractButton*>("revealAttributeButton"), Qt::LeftButton);
  770. QCOMPARE(attrTextEdit->toPlainText(), expectedFinalSeed);
  771. auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
  772. QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
  773. // Test the TOTP value
  774. triggerAction("actionEntryTotp");
  775. auto* totpDialog = m_dbWidget->findChild<TotpDialog*>("TotpDialog");
  776. auto* totpLabel = totpDialog->findChild<QLabel*>("totpLabel");
  777. QTRY_COMPARE(totpLabel->text().replace(" ", ""), entry->totp());
  778. QTest::keyClick(totpDialog, Qt::Key_Escape);
  779. // Test the QR code
  780. triggerAction("actionEntryTotpQRCode");
  781. auto* qrCodeDialog = m_mainWindow->findChild<QDialog*>("entryQrCodeWidget");
  782. QVERIFY(qrCodeDialog);
  783. QVERIFY(qrCodeDialog->isVisible());
  784. auto* qrCodeWidget = qrCodeDialog->findChild<QWidget*>("squareSvgWidget");
  785. QVERIFY2(qrCodeWidget->geometry().width() == qrCodeWidget->geometry().height(), "Initial QR code is not square");
  786. // Test the QR code window resizing, make the dialog bigger.
  787. qrCodeDialog->setFixedSize(800, 600);
  788. QVERIFY2(qrCodeWidget->geometry().width() == qrCodeWidget->geometry().height(), "Resized QR code is not square");
  789. QTest::keyClick(qrCodeDialog, Qt::Key_Escape);
  790. }
  791. void TestGui::testSearch()
  792. {
  793. // Add canned entries for consistent testing
  794. addCannedEntries();
  795. auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
  796. auto* searchWidget = toolBar->findChild<SearchWidget*>("SearchWidget");
  797. QVERIFY(searchWidget->isEnabled());
  798. auto* searchTextEdit = searchWidget->findChild<QLineEdit*>("searchEdit");
  799. auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
  800. QVERIFY(entryView->isVisible());
  801. QVERIFY(searchTextEdit->isClearButtonEnabled());
  802. auto* helpButton = searchWidget->findChild<QAction*>("helpIcon");
  803. auto* helpPanel = searchWidget->findChild<QWidget*>("SearchHelpWidget");
  804. QVERIFY(helpButton->isVisible());
  805. QVERIFY(!helpPanel->isVisible());
  806. // Enter search
  807. QTest::mouseClick(searchTextEdit, Qt::LeftButton);
  808. QTRY_VERIFY(searchTextEdit->hasFocus());
  809. // Show/Hide search help
  810. helpButton->trigger();
  811. QTRY_VERIFY(helpPanel->isVisible());
  812. QTest::mouseClick(searchTextEdit, Qt::LeftButton);
  813. QTRY_VERIFY(helpPanel->isVisible());
  814. QApplication::processEvents();
  815. helpButton->trigger();
  816. QTRY_VERIFY(!helpPanel->isVisible());
  817. // Need to re-activate the window after the help test
  818. m_mainWindow->activateWindow();
  819. // Search for "ZZZ"
  820. QTest::keyClicks(searchTextEdit, "ZZZ");
  821. QTRY_COMPARE(searchTextEdit->text(), QString("ZZZ"));
  822. QTRY_VERIFY(m_dbWidget->isSearchActive());
  823. QTRY_COMPARE(entryView->model()->rowCount(), 0);
  824. // Press the search clear button
  825. searchTextEdit->clear();
  826. QTRY_VERIFY(searchTextEdit->text().isEmpty());
  827. QTRY_VERIFY(searchTextEdit->hasFocus());
  828. // Test tag search
  829. searchTextEdit->clear();
  830. QTest::keyClicks(searchTextEdit, "tag: testTag");
  831. QTRY_VERIFY(m_dbWidget->isSearchActive());
  832. QTRY_COMPARE(entryView->model()->rowCount(), 1);
  833. searchTextEdit->clear();
  834. QTRY_VERIFY(searchTextEdit->text().isEmpty());
  835. QTRY_VERIFY(searchTextEdit->hasFocus());
  836. // Escape clears searchedit and retains focus
  837. QTest::keyClicks(searchTextEdit, "ZZZ");
  838. QTest::keyClick(searchTextEdit, Qt::Key_Escape);
  839. QTRY_VERIFY(searchTextEdit->text().isEmpty());
  840. QTRY_VERIFY(searchTextEdit->hasFocus());
  841. QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
  842. // Search for "some"
  843. QTest::keyClicks(searchTextEdit, "some");
  844. QTRY_VERIFY(m_dbWidget->isSearchActive());
  845. QTRY_COMPARE(entryView->model()->rowCount(), 3);
  846. // Search for "someTHING"
  847. QTest::keyClicks(searchTextEdit, "THING");
  848. QTRY_COMPARE(entryView->model()->rowCount(), 2);
  849. // Press Down to focus on the entry view
  850. QTest::keyClick(searchTextEdit, Qt::Key_Right, Qt::ControlModifier);
  851. QTRY_VERIFY(searchTextEdit->hasFocus());
  852. QTest::keyClick(searchTextEdit, Qt::Key_Down);
  853. QTRY_VERIFY(entryView->hasFocus());
  854. auto* searchedEntry = entryView->currentEntry();
  855. // Restore focus using F3 key and search text selection
  856. QTest::keyClick(m_mainWindow.data(), Qt::Key_F3);
  857. QTRY_VERIFY(searchTextEdit->hasFocus());
  858. QTRY_COMPARE(searchTextEdit->selectedText(), QString("someTHING"));
  859. searchedEntry->setPassword("password");
  860. QClipboard* clipboard = QApplication::clipboard();
  861. // Copy to clipboard: should copy search text (not password)
  862. QTest::keyClick(searchTextEdit, Qt::Key_C, Qt::ControlModifier);
  863. QCOMPARE(clipboard->text(), QString("someTHING"));
  864. // Deselect text and confirm password copies
  865. QTest::mouseClick(searchTextEdit, Qt::LeftButton);
  866. QTRY_VERIFY(searchTextEdit->selectedText().isEmpty());
  867. QTRY_VERIFY(searchTextEdit->hasFocus());
  868. QTest::keyClick(searchTextEdit, Qt::Key_C, Qt::ControlModifier);
  869. QCOMPARE(clipboard->text(), searchedEntry->password());
  870. // Ensure Down focuses on entry view when search text is selected
  871. QTest::keyClick(searchTextEdit, Qt::Key_A, Qt::ControlModifier);
  872. QTest::keyClick(searchTextEdit, Qt::Key_Down);
  873. QTRY_VERIFY(entryView->hasFocus());
  874. QCOMPARE(entryView->currentEntry(), searchedEntry);
  875. // Test that password copies with entry focused
  876. QTest::keyClick(entryView, Qt::Key_C, Qt::ControlModifier);
  877. QCOMPARE(clipboard->text(), searchedEntry->password());
  878. // Refocus back to search edit
  879. QTest::mouseClick(searchTextEdit, Qt::LeftButton);
  880. QTRY_VERIFY(searchTextEdit->hasFocus());
  881. // Select search text and test that password does not copy
  882. searchTextEdit->selectAll();
  883. QTest::keyClick(searchTextEdit, Qt::Key_C, Qt::ControlModifier);
  884. QTRY_COMPARE(clipboard->text(), QString("someTHING"));
  885. // Ensure password copies when clicking on copy password button despite selected text
  886. auto copyPasswordAction = m_mainWindow->findChild<QAction*>("actionEntryCopyPassword");
  887. QVERIFY(copyPasswordAction);
  888. auto copyPasswordWidget = toolBar->widgetForAction(copyPasswordAction);
  889. QVERIFY(copyPasswordWidget);
  890. QTest::mouseClick(copyPasswordWidget, Qt::LeftButton);
  891. QCOMPARE(clipboard->text(), searchedEntry->password());
  892. // Deselect text and deselect entry, Ctrl+C should now do nothing
  893. clipboard->clear();
  894. QTest::mouseClick(searchTextEdit, Qt::LeftButton);
  895. entryView->clearSelection();
  896. QTest::keyClick(searchTextEdit, Qt::Key_C, Qt::ControlModifier);
  897. QCOMPARE(clipboard->text(), QString());
  898. // Test case sensitive search
  899. searchWidget->setCaseSensitive(true);
  900. QTRY_COMPARE(entryView->model()->rowCount(), 0);
  901. searchWidget->setCaseSensitive(false);
  902. QTRY_COMPARE(entryView->model()->rowCount(), 2);
  903. // Test group search
  904. searchWidget->setLimitGroup(false);
  905. GroupView* groupView = m_dbWidget->findChild<GroupView*>("groupView");
  906. QCOMPARE(groupView->currentGroup(), m_db->rootGroup());
  907. QModelIndex rootGroupIndex = groupView->model()->index(0, 0);
  908. clickIndex(groupView->model()->index(0, 0, rootGroupIndex), groupView, Qt::LeftButton);
  909. QCOMPARE(groupView->currentGroup()->name(), QString("General"));
  910. // Selecting a group should cancel search
  911. QTRY_COMPARE(entryView->model()->rowCount(), 0);
  912. // Restore search
  913. QTest::keyClick(m_mainWindow.data(), Qt::Key_F, Qt::ControlModifier);
  914. QTest::keyClicks(searchTextEdit, "someTHING");
  915. QTRY_COMPARE(entryView->model()->rowCount(), 2);
  916. // Enable group limiting
  917. searchWidget->setLimitGroup(true);
  918. QTRY_COMPARE(entryView->model()->rowCount(), 0);
  919. // Selecting another group should NOT cancel search
  920. clickIndex(rootGroupIndex, groupView, Qt::LeftButton);
  921. QCOMPARE(groupView->currentGroup(), m_db->rootGroup());
  922. QTRY_COMPARE(entryView->model()->rowCount(), 2);
  923. // reset
  924. searchWidget->setLimitGroup(false);
  925. clickIndex(rootGroupIndex, groupView, Qt::LeftButton);
  926. QCOMPARE(groupView->currentGroup(), m_db->rootGroup());
  927. QVERIFY(!m_dbWidget->isSearchActive());
  928. // check if first entry is selected after search
  929. QTest::keyClicks(searchTextEdit, "some");
  930. QTRY_VERIFY(m_dbWidget->isSearchActive());
  931. QTRY_COMPARE(entryView->selectedEntries().length(), 1);
  932. QModelIndex index_current = entryView->indexFromEntry(entryView->currentEntry());
  933. QTRY_COMPARE(index_current.row(), 0);
  934. // Try to edit the first entry from the search view
  935. // Refocus back to search edit
  936. QTest::mouseClick(searchTextEdit, Qt::LeftButton);
  937. QTRY_VERIFY(searchTextEdit->hasFocus());
  938. QTest::keyClicks(searchTextEdit, "someTHING");
  939. QTRY_VERIFY(m_dbWidget->isSearchActive());
  940. QModelIndex item = entryView->model()->index(0, 1);
  941. Entry* entry = entryView->entryFromIndex(item);
  942. QTest::keyClick(searchTextEdit, Qt::Key_Return);
  943. QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditEntryMode);
  944. // Perform the edit and save it
  945. EditEntryWidget* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
  946. QLineEdit* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
  947. QString origTitle = titleEdit->text();
  948. QTest::keyClicks(titleEdit, "_edited");
  949. QDialogButtonBox* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
  950. QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
  951. // Confirm the edit was made and we are back in search mode
  952. QTRY_VERIFY(m_dbWidget->isSearchActive());
  953. QCOMPARE(entry->title(), origTitle.append("_edited"));
  954. // Cancel search, should return to normal view
  955. QTest::keyClick(m_mainWindow.data(), Qt::Key_Escape);
  956. QTRY_COMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
  957. }
  958. void TestGui::testDeleteEntry()
  959. {
  960. // Add canned entries for consistent testing
  961. addCannedEntries();
  962. checkStatusBarText("4 Ent");
  963. auto* groupView = m_dbWidget->findChild<GroupView*>("groupView");
  964. auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
  965. auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
  966. auto* entryDeleteAction = m_mainWindow->findChild<QAction*>("actionEntryDelete");
  967. QWidget* entryDeleteWidget = toolBar->widgetForAction(entryDeleteAction);
  968. entryView->setFocus();
  969. // Move one entry to the recycling bin
  970. QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
  971. clickIndex(entryView->model()->index(1, 1), entryView, Qt::LeftButton);
  972. QVERIFY(entryDeleteWidget->isVisible());
  973. QVERIFY(entryDeleteWidget->isEnabled());
  974. QVERIFY(!m_db->metadata()->recycleBin());
  975. // Test with confirmation dialog
  976. if (!config()->get(Config::Security_NoConfirmMoveEntryToRecycleBin).toBool()) {
  977. MessageBox::setNextAnswer(MessageBox::Move);
  978. QTest::mouseClick(entryDeleteWidget, Qt::LeftButton);
  979. QCOMPARE(entryView->model()->rowCount(), 3);
  980. QCOMPARE(m_db->metadata()->recycleBin()->entries().size(), 1);
  981. } else {
  982. // no confirm dialog
  983. QTest::mouseClick(entryDeleteWidget, Qt::LeftButton);
  984. QCOMPARE(entryView->model()->rowCount(), 3);
  985. QCOMPARE(m_db->metadata()->recycleBin()->entries().size(), 1);
  986. }
  987. checkStatusBarText("3 Ent");
  988. // Select multiple entries and move them to the recycling bin
  989. clickIndex(entryView->model()->index(1, 1), entryView, Qt::LeftButton);
  990. clickIndex(entryView->model()->index(2, 1), entryView, Qt::LeftButton, Qt::ControlModifier);
  991. QCOMPARE(entryView->selectionModel()->selectedRows().size(), 2);
  992. if (!config()->get(Config::Security_NoConfirmMoveEntryToRecycleBin).toBool()) {
  993. MessageBox::setNextAnswer(MessageBox::Cancel);
  994. QTest::mouseClick(entryDeleteWidget, Qt::LeftButton);
  995. QCOMPARE(entryView->model()->rowCount(), 3);
  996. QCOMPARE(m_db->metadata()->recycleBin()->entries().size(), 1);
  997. MessageBox::setNextAnswer(MessageBox::Move);
  998. QTest::mouseClick(entryDeleteWidget, Qt::LeftButton);
  999. QCOMPARE(entryView->model()->rowCount(), 1);
  1000. QCOMPARE(m_db->metadata()->recycleBin()->entries().size(), 3);
  1001. } else {
  1002. QTest::mouseClick(entryDeleteWidget, Qt::LeftButton);
  1003. QCOMPARE(entryView->model()->rowCount(), 1);
  1004. QCOMPARE(m_db->metadata()->recycleBin()->entries().size(), 3);
  1005. }
  1006. // Go to the recycling bin
  1007. QCOMPARE(groupView->currentGroup(), m_db->rootGroup());
  1008. QModelIndex rootGroupIndex = groupView->model()->index(0, 0);
  1009. clickIndex(groupView->model()->index(groupView->model()->rowCount(rootGroupIndex) - 1, 0, rootGroupIndex),
  1010. groupView,
  1011. Qt::LeftButton);
  1012. QCOMPARE(groupView->currentGroup()->name(), m_db->metadata()->recycleBin()->name());
  1013. // Delete one entry from the bin
  1014. clickIndex(entryView->model()->index(0, 1), entryView, Qt::LeftButton);
  1015. MessageBox::setNextAnswer(MessageBox::Cancel);
  1016. QTest::mouseClick(entryDeleteWidget, Qt::LeftButton);
  1017. QCOMPARE(entryView->model()->rowCount(), 3);
  1018. QCOMPARE(m_db->metadata()->recycleBin()->entries().size(), 3);
  1019. MessageBox::setNextAnswer(MessageBox::Delete);
  1020. QTest::mouseClick(entryDeleteWidget, Qt::LeftButton);
  1021. QCOMPARE(entryView->model()->rowCount(), 2);
  1022. QCOMPARE(m_db->metadata()->recycleBin()->entries().size(), 2);
  1023. // Select the remaining entries and delete them
  1024. clickIndex(entryView->model()->index(0, 1), entryView, Qt::LeftButton);
  1025. clickIndex(entryView->model()->index(1, 1), entryView, Qt::LeftButton, Qt::ControlModifier);
  1026. MessageBox::setNextAnswer(MessageBox::Delete);
  1027. QTest::mouseClick(entryDeleteWidget, Qt::LeftButton);
  1028. QCOMPARE(entryView->model()->rowCount(), 0);
  1029. QCOMPARE(m_db->metadata()->recycleBin()->entries().size(), 0);
  1030. // Ensure the entry preview widget shows the recycling group since all entries are deleted
  1031. auto* previewWidget = m_dbWidget->findChild<EntryPreviewWidget*>("previewWidget");
  1032. QVERIFY(previewWidget);
  1033. auto* groupTitleLabel = previewWidget->findChild<QLabel*>("groupTitleLabel");
  1034. QVERIFY(groupTitleLabel);
  1035. QTRY_VERIFY(groupTitleLabel->isVisible());
  1036. QVERIFY(groupTitleLabel->text().contains(m_db->metadata()->recycleBin()->name()));
  1037. // Go back to the root group
  1038. clickIndex(groupView->model()->index(0, 0), groupView, Qt::LeftButton);
  1039. QCOMPARE(groupView->currentGroup(), m_db->rootGroup());
  1040. }
  1041. void TestGui::testCloneEntry()
  1042. {
  1043. auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
  1044. entryView->setFocus();
  1045. QCOMPARE(entryView->model()->rowCount(), 1);
  1046. QModelIndex item = entryView->model()->index(0, 1);
  1047. Entry* entryOrg = entryView->entryFromIndex(item);
  1048. clickIndex(item, entryView, Qt::LeftButton);
  1049. triggerAction("actionEntryClone");
  1050. auto* cloneDialog = m_dbWidget->findChild<CloneDialog*>("CloneDialog");
  1051. auto* cloneButtonBox = cloneDialog->findChild<QDialogButtonBox*>("buttonBox");
  1052. QTest::mouseClick(cloneButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
  1053. QCOMPARE(entryView->model()->rowCount(), 2);
  1054. Entry* entryClone = entryView->entryFromIndex(entryView->model()->index(1, 1));
  1055. QVERIFY(entryOrg->uuid() != entryClone->uuid());
  1056. QCOMPARE(entryClone->title(), entryOrg->title() + QString(" - Clone"));
  1057. QVERIFY(m_dbWidget->currentSelectedEntry()->uuid() == entryClone->uuid());
  1058. }
  1059. void TestGui::testEntryPlaceholders()
  1060. {
  1061. auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
  1062. auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
  1063. // Find the new entry action
  1064. auto* entryNewAction = m_mainWindow->findChild<QAction*>("actionEntryNew");
  1065. QVERIFY(entryNewAction->isEnabled());
  1066. // Find the button associated with the new entry action
  1067. QWidget* entryNewWidget = toolBar->widgetForAction(entryNewAction);
  1068. QVERIFY(entryNewWidget->isVisible());
  1069. QVERIFY(entryNewWidget->isEnabled());
  1070. // Click the new entry button and check that we enter edit mode
  1071. QTest::mouseClick(entryNewWidget, Qt::LeftButton);
  1072. QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditEntryMode);
  1073. // Add entry "test" and confirm added
  1074. auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
  1075. auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
  1076. QTest::keyClicks(titleEdit, "test");
  1077. QComboBox* usernameComboBox = editEntryWidget->findChild<QComboBox*>("usernameComboBox");
  1078. QTest::keyClicks(usernameComboBox, "john");
  1079. QLineEdit* urlEdit = editEntryWidget->findChild<QLineEdit*>("urlEdit");
  1080. QTest::keyClicks(urlEdit, "{TITLE}.{USERNAME}");
  1081. auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
  1082. QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
  1083. QCOMPARE(entryView->model()->rowCount(), 2);
  1084. QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
  1085. QModelIndex item = entryView->model()->index(1, 1);
  1086. Entry* entry = entryView->entryFromIndex(item);
  1087. QCOMPARE(entry->title(), QString("test"));
  1088. QCOMPARE(entry->url(), QString("{TITLE}.{USERNAME}"));
  1089. // Test password copy
  1090. QClipboard* clipboard = QApplication::clipboard();
  1091. m_dbWidget->copyURL();
  1092. QTRY_COMPARE(clipboard->text(), QString("test.john"));
  1093. }
  1094. void TestGui::testDragAndDropEntry()
  1095. {
  1096. auto entryView = m_dbWidget->findChild<EntryView*>("entryView");
  1097. auto groupView = m_dbWidget->findChild<GroupView*>("groupView");
  1098. auto groupModel = qobject_cast<GroupModel*>(groupView->model());
  1099. QModelIndex sourceIndex = entryView->model()->index(0, 1);
  1100. QModelIndex targetIndex = groupModel->index(0, 0, groupModel->index(0, 0));
  1101. QVERIFY(sourceIndex.isValid());
  1102. QVERIFY(targetIndex.isValid());
  1103. auto targetGroup = groupModel->groupFromIndex(targetIndex);
  1104. QMimeData mimeData;
  1105. QByteArray encoded;
  1106. QDataStream stream(&encoded, QIODevice::WriteOnly);
  1107. auto entry = entryView->entryFromIndex(sourceIndex);
  1108. stream << entry->group()->database()->uuid() << entry->uuid();
  1109. mimeData.setData("application/x-keepassx-entry", encoded);
  1110. // Test Copy, UUID should change, history remain
  1111. QVERIFY(groupModel->dropMimeData(&mimeData, Qt::CopyAction, -1, 0, targetIndex));
  1112. // Find the copied entry
  1113. auto newEntry = targetGroup->findEntryByPath(entry->title());
  1114. QVERIFY(newEntry);
  1115. QVERIFY(entry->uuid() != newEntry->uuid());
  1116. QCOMPARE(entry->historyItems().count(), newEntry->historyItems().count());
  1117. encoded.clear();
  1118. entry = entryView->entryFromIndex(sourceIndex);
  1119. auto history = entry->historyItems().count();
  1120. auto uuid = entry->uuid();
  1121. stream << entry->group()->database()->uuid() << entry->uuid();
  1122. mimeData.setData("application/x-keepassx-entry", encoded);
  1123. // Test Move, entry pointer should remain the same
  1124. QCOMPARE(entry->group()->name(), QString("NewDatabase"));
  1125. QVERIFY(groupModel->dropMimeData(&mimeData, Qt::MoveAction, -1, 0, targetIndex));
  1126. QCOMPARE(entry->group()->name(), QString("General"));
  1127. QCOMPARE(entry->uuid(), uuid);
  1128. QCOMPARE(entry->historyItems().count(), history);
  1129. }
  1130. void TestGui::testDragAndDropGroup()
  1131. {
  1132. QAbstractItemModel* groupModel = m_dbWidget->findChild<GroupView*>("groupView")->model();
  1133. QModelIndex rootIndex = groupModel->index(0, 0);
  1134. dragAndDropGroup(groupModel->index(0, 0, rootIndex), groupModel->index(1, 0, rootIndex), -1, true, "Windows", 0);
  1135. // dropping parent on child is supposed to fail
  1136. dragAndDropGroup(groupModel->index(0, 0, rootIndex),
  1137. groupModel->index(0, 0, groupModel->index(0, 0, rootIndex)),
  1138. -1,
  1139. false,
  1140. "NewDatabase",
  1141. 0);
  1142. dragAndDropGroup(groupModel->index(1, 0, rootIndex), rootIndex, 0, true, "NewDatabase", 0);
  1143. dragAndDropGroup(groupModel->index(0, 0, rootIndex), rootIndex, -1, true, "NewDatabase", 4);
  1144. }
  1145. void TestGui::testSaveAs()
  1146. {
  1147. QFileInfo fileInfo(m_dbFilePath);
  1148. QDateTime lastModified = fileInfo.lastModified();
  1149. m_db->metadata()->setName("testSaveAs");
  1150. // open temporary file so it creates a filename
  1151. TemporaryFile tmpFile;
  1152. QVERIFY(tmpFile.open());
  1153. QString tmpFileName = tmpFile.fileName();
  1154. tmpFile.remove();
  1155. fileDialog()->setNextFileName(tmpFileName);
  1156. triggerAction("actionDatabaseSaveAs");
  1157. QCOMPARE(m_tabWidget->tabText(m_tabWidget->currentIndex()), QString("testSaveAs"));
  1158. checkDatabase(tmpFileName);
  1159. fileInfo.refresh();
  1160. QCOMPARE(fileInfo.lastModified(), lastModified);
  1161. tmpFile.remove();
  1162. }
  1163. void TestGui::testSaveBackup()
  1164. {
  1165. m_db->metadata()->setName("testSaveBackup");
  1166. QFileInfo fileInfo(m_dbFilePath);
  1167. QDateTime lastModified = fileInfo.lastModified();
  1168. // open temporary file so it creates a filename
  1169. TemporaryFile tmpFile;
  1170. QVERIFY(tmpFile.open());
  1171. QString tmpFileName = tmpFile.fileName();
  1172. tmpFile.remove();
  1173. // wait for modified timer
  1174. QTRY_COMPARE(m_tabWidget->tabText(m_tabWidget->currentIndex()), QString("testSaveBackup*"));
  1175. fileDialog()->setNextFileName(tmpFileName);
  1176. triggerAction("actionDatabaseSaveBackup");
  1177. QTRY_COMPARE(m_tabWidget->tabText(m_tabWidget->currentIndex()), QString("testSaveBackup*"));
  1178. checkDatabase(tmpFileName);
  1179. fileInfo.refresh();
  1180. QCOMPARE(fileInfo.lastModified(), lastModified);
  1181. tmpFile.remove();
  1182. }
  1183. void TestGui::testSave()
  1184. {
  1185. // Make a modification to the database then save
  1186. m_db->metadata()->setName("testSave");
  1187. checkSaveDatabase();
  1188. }
  1189. void TestGui::testSaveBackupPath_data()
  1190. {
  1191. QTest::addColumn<QString>("backupFilePathPattern");
  1192. QTest::addColumn<QString>("expectedBackupFile");
  1193. // Absolute paths should remain absolute
  1194. TemporaryFile tmpFile;
  1195. QVERIFY(tmpFile.open());
  1196. tmpFile.remove();
  1197. QTest::newRow("Absolute backup path") << tmpFile.fileName() << tmpFile.fileName();
  1198. // relative paths should be resolved to database parent directory
  1199. QTest::newRow("Relative backup path (implicit)") << "other_dir/test.old.kdbx" << "other_dir/test.old.kdbx";
  1200. QTest::newRow("Relative backup path (explicit)") << "./other_dir2/test2.old.kdbx" << "other_dir2/test2.old.kdbx";
  1201. QTest::newRow("Path with placeholders") << "{DB_FILENAME}.old.kdbx" << "KeePassXC.old.kdbx";
  1202. // empty path should be replaced with default pattern
  1203. QTest::newRow("Empty path") << QString("") << config()->getDefault(Config::BackupFilePathPattern).toString();
  1204. // {DB_FILENAME} should be replaced with database filename
  1205. QTest::newRow("") << "{DB_FILENAME}_.old.kdbx" << "{DB_FILENAME}_.old.kdbx";
  1206. }
  1207. void TestGui::testSaveBackupPath()
  1208. {
  1209. /**
  1210. * Tests that the backupFilePathPattern config entry is respected. We do not test patterns like {TIME} etc here
  1211. * as this is done in a separate test case. We do however check {DB_FILENAME} as this is a feature of the
  1212. * performBackup() function.
  1213. */
  1214. // Get test data
  1215. QFETCH(QString, backupFilePathPattern);
  1216. QFETCH(QString, expectedBackupFile);
  1217. // Enable automatic backups
  1218. config()->set(Config::BackupBeforeSave, true);
  1219. config()->set(Config::BackupFilePathPattern, backupFilePathPattern);
  1220. // Replace placeholders and resolve relative paths. This cannot be done in the _data() function as the
  1221. // db path/filename is not known yet
  1222. auto dbFileInfo = QFileInfo(m_dbFilePath);
  1223. if (!QDir::isAbsolutePath(expectedBackupFile)) {
  1224. expectedBackupFile = QDir(dbFileInfo.absolutePath()).absoluteFilePath(expectedBackupFile);
  1225. }
  1226. expectedBackupFile.replace("{DB_FILENAME}", dbFileInfo.completeBaseName());
  1227. // Save a modified database
  1228. auto prevName = m_db->metadata()->name();
  1229. m_db->metadata()->setName("testBackupPathPattern");
  1230. checkSaveDatabase();
  1231. // Test that the backup file has the previous database name
  1232. checkDatabase(expectedBackupFile, prevName);
  1233. // Clean up
  1234. QFile(expectedBackupFile).remove();
  1235. }
  1236. void TestGui::testDatabaseSettings()
  1237. {
  1238. m_db->metadata()->setName("testDatabaseSettings");
  1239. triggerAction("actionDatabaseSettings");
  1240. auto* dbSettingsDialog = m_dbWidget->findChild<QWidget*>("databaseSettingsDialog");
  1241. auto* dbSettingsCategoryList = dbSettingsDialog->findChild<CategoryListWidget*>("categoryList");
  1242. auto* dbSettingsStackedWidget = dbSettingsDialog->findChild<QStackedWidget*>("stackedWidget");
  1243. auto* autosaveDelayCheckBox = dbSettingsDialog->findChild<QCheckBox*>("autosaveDelayCheckBox");
  1244. auto* autosaveDelaySpinBox = dbSettingsDialog->findChild<QSpinBox*>("autosaveDelaySpinBox");
  1245. auto* dbSettingsButtonBox = dbSettingsDialog->findChild<QDialogButtonBox*>("buttonBox");
  1246. int autosaveDelayTestValue = 2;
  1247. dbSettingsCategoryList->setCurrentCategory(1); // go into security category
  1248. auto securityTabWidget = dbSettingsStackedWidget->findChild<QTabWidget*>("securityTabWidget");
  1249. QCOMPARE(securityTabWidget->currentIndex(), 0);
  1250. // Interact with the password edit option
  1251. auto passwordEditWidget = securityTabWidget->findChild<PasswordEditWidget*>();
  1252. QVERIFY(passwordEditWidget);
  1253. auto editPasswordButton = passwordEditWidget->findChild<QPushButton*>("changeButton");
  1254. QVERIFY(editPasswordButton);
  1255. QVERIFY(editPasswordButton->isVisible());
  1256. QTest::mouseClick(editPasswordButton, Qt::LeftButton);
  1257. QApplication::processEvents();
  1258. auto passwordWidgets = dbSettingsDialog->findChildren<PasswordWidget*>();
  1259. QVERIFY(passwordWidgets.count() == 2);
  1260. QVERIFY(passwordWidgets[0]->isVisible());
  1261. passwordWidgets[0]->setText("b");
  1262. passwordWidgets[1]->setText("b");
  1263. // Toggle between tabs to ensure the password remains
  1264. securityTabWidget->setCurrentIndex(1);
  1265. QApplication::processEvents();
  1266. securityTabWidget->setCurrentIndex(0);
  1267. QApplication::processEvents();
  1268. QCOMPARE(passwordWidgets[0]->text(), QString("b"));
  1269. // Cancel password change and confirm password is cleared
  1270. auto cancelPasswordButton = passwordEditWidget->findChild<QPushButton*>("cancelButton");
  1271. QVERIFY(cancelPasswordButton);
  1272. QTest::mouseClick(cancelPasswordButton, Qt::LeftButton);
  1273. QApplication::processEvents();
  1274. QVERIFY(!passwordWidgets[0]->isVisible());
  1275. QCOMPARE(passwordWidgets[0]->text(), QString(""));
  1276. QVERIFY(editPasswordButton->isVisible());
  1277. // Switch to encryption tab and interact with various settings
  1278. securityTabWidget->setCurrentIndex(1);
  1279. QApplication::processEvents();
  1280. // Verify database is KDBX3
  1281. auto compatibilitySelection = securityTabWidget->findChild<QComboBox*>("compatibilitySelection");
  1282. QVERIFY(compatibilitySelection);
  1283. QVERIFY(compatibilitySelection->isEnabled());
  1284. QCOMPARE(compatibilitySelection->currentText(), QString("KDBX 3"));
  1285. // Verify advanced settings
  1286. auto encryptionSettings = securityTabWidget->findChild<QTabWidget*>("encryptionSettingsTabWidget");
  1287. auto advancedTab = encryptionSettings->findChild<QWidget*>("advancedTab");
  1288. encryptionSettings->setCurrentWidget(advancedTab);
  1289. QApplication::processEvents();
  1290. // Verify KDF is AES KDBX3
  1291. auto kdfSelection = advancedTab->findChild<QComboBox*>("kdfComboBox");
  1292. QVERIFY(kdfSelection->isVisible());
  1293. QCOMPARE(kdfSelection->currentText(), QString("AES-KDF (KDBX 3)"));
  1294. auto transformRoundsSpinBox = advancedTab->findChild<QSpinBox*>("transformRoundsSpinBox");
  1295. QVERIFY(transformRoundsSpinBox);
  1296. // Adjust compatibility to KDBX4 and wait for KDF to update
  1297. compatibilitySelection->setCurrentIndex(0);
  1298. QTRY_VERIFY(transformRoundsSpinBox->isEnabled());
  1299. QCOMPARE(compatibilitySelection->currentText().left(6), QString("KDBX 4"));
  1300. QCOMPARE(kdfSelection->currentText().left(7), QString("Argon2d"));
  1301. // Switch to AES KDBX4, change rounds, then accept
  1302. kdfSelection->setCurrentIndex(2);
  1303. QCOMPARE(kdfSelection->currentText(), QString("AES-KDF (KDBX 4)"));
  1304. transformRoundsSpinBox->setValue(123456);
  1305. QTest::keyClick(transformRoundsSpinBox, Qt::Key_Enter);
  1306. QTRY_COMPARE(m_db->kdf()->rounds(), 123456);
  1307. QVERIFY(m_db->formatVersion() >= KeePass2::FILE_VERSION_4);
  1308. QCOMPARE(m_db->kdf()->uuid(), KeePass2::KDF_AES_KDBX4);
  1309. // Go back into database settings
  1310. triggerAction("actionDatabaseSettings");
  1311. // test disable and default values for maximum history items and size
  1312. auto* historyMaxItemsCheckBox = dbSettingsDialog->findChild<QCheckBox*>("historyMaxItemsCheckBox");
  1313. auto* historyMaxItemsSpinBox = dbSettingsDialog->findChild<QSpinBox*>("historyMaxItemsSpinBox");
  1314. auto* historyMaxSizeCheckBox = dbSettingsDialog->findChild<QCheckBox*>("historyMaxSizeCheckBox");
  1315. auto* historyMaxSizeSpinBox = dbSettingsDialog->findChild<QSpinBox*>("historyMaxSizeSpinBox");
  1316. // test defaults
  1317. QCOMPARE(historyMaxItemsSpinBox->value(), Metadata::DefaultHistoryMaxItems);
  1318. QCOMPARE(historyMaxSizeSpinBox->value(), qRound(Metadata::DefaultHistoryMaxSize / qreal(1024 * 1024)));
  1319. // disable and test setting as well
  1320. historyMaxItemsCheckBox->setChecked(false);
  1321. historyMaxSizeCheckBox->setChecked(false);
  1322. QTest::mouseClick(dbSettingsButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
  1323. QTRY_COMPARE(m_db->metadata()->historyMaxItems(), -1);
  1324. QTRY_COMPARE(m_db->metadata()->historyMaxSize(), -1);
  1325. // then open to check the saved disabled state in gui
  1326. triggerAction("actionDatabaseSettings");
  1327. QCOMPARE(historyMaxItemsCheckBox->isChecked(), false);
  1328. QCOMPARE(historyMaxSizeCheckBox->isChecked(), false);
  1329. QTest::mouseClick(dbSettingsButtonBox->button(QDialogButtonBox::Cancel), Qt::LeftButton);
  1330. // Test loading default values and setting autosaveDelay
  1331. triggerAction("actionDatabaseSettings");
  1332. QVERIFY(autosaveDelayCheckBox->isChecked() == false);
  1333. autosaveDelayCheckBox->toggle();
  1334. autosaveDelaySpinBox->setValue(autosaveDelayTestValue);
  1335. QTest::mouseClick(dbSettingsButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
  1336. QTRY_COMPARE(m_db->metadata()->autosaveDelayMin(), autosaveDelayTestValue);
  1337. checkSaveDatabase();
  1338. // Test loading autosaveDelay non-default values
  1339. triggerAction("actionDatabaseSettings");
  1340. QTRY_COMPARE(autosaveDelayCheckBox->isChecked(), true);
  1341. QTRY_COMPARE(autosaveDelaySpinBox->value(), autosaveDelayTestValue);
  1342. QTest::mouseClick(dbSettingsButtonBox->button(QDialogButtonBox::Cancel), Qt::LeftButton);
  1343. // test autosave delay
  1344. // 1 init
  1345. config()->set(Config::AutoSaveAfterEveryChange, true);
  1346. QSignalSpy writeDbSignalSpy(m_db.data(), &Database::databaseSaved);
  1347. // 2 create new entries
  1348. // 2.a) Click the new entry button and set the title
  1349. auto* entryNewAction = m_mainWindow->findChild<QAction*>("actionEntryNew");
  1350. QVERIFY(entryNewAction->isEnabled());
  1351. auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
  1352. QVERIFY(toolBar);
  1353. QWidget* entryNewWidget = toolBar->widgetForAction(entryNewAction);
  1354. QTest::mouseClick(entryNewWidget, Qt::LeftButton);
  1355. QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditEntryMode);
  1356. auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
  1357. QVERIFY(editEntryWidget);
  1358. auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
  1359. QVERIFY(titleEdit);
  1360. QTest::keyClicks(titleEdit, "Test autosaveDelay 1");
  1361. // 2.b) Save changes
  1362. editEntryWidget->switchToPage(EditEntryWidget::Page::Main);
  1363. auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
  1364. QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
  1365. // 2.c) Make sure file was not modified yet
  1366. Tools::wait(150); // due to modify timer
  1367. QTRY_COMPARE(writeDbSignalSpy.count(), 0);
  1368. // 2.d) Create second entry to test delay timer reset
  1369. QTest::mouseClick(entryNewWidget, Qt::LeftButton);
  1370. QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditEntryMode);
  1371. QTest::keyClicks(titleEdit, "Test autosaveDelay 2");
  1372. // 2.e) Save changes
  1373. editEntryWidget->switchToPage(EditEntryWidget::Page::Main);
  1374. editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
  1375. QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
  1376. // 3 Double check both true negative and true positive
  1377. // 3.a) Test unmodified prior to delay timeout
  1378. Tools::wait(150); // due to modify timer
  1379. QTRY_COMPARE(writeDbSignalSpy.count(), 0);
  1380. // 3.b) Test modification time after expected
  1381. m_dbWidget->triggerAutosaveTimer();
  1382. QTRY_COMPARE(writeDbSignalSpy.count(), 1);
  1383. // 4 Test no delay when disabled autosave or autosaveDelay
  1384. // 4.a) create new entry
  1385. QTest::mouseClick(entryNewWidget, Qt::LeftButton);
  1386. QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditEntryMode);
  1387. QTest::keyClicks(titleEdit, "Test autosaveDelay 3");
  1388. // 4.b) Save changes
  1389. editEntryWidget->switchToPage(EditEntryWidget::Page::Main);
  1390. editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
  1391. QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
  1392. // 4.c) Start timer
  1393. Tools::wait(150); // due to modify timer
  1394. // 4.d) Disable autosave
  1395. config()->set(Config::AutoSaveAfterEveryChange, false);
  1396. // 4.e) Make sure changes are not saved
  1397. m_dbWidget->triggerAutosaveTimer();
  1398. QTRY_COMPARE(writeDbSignalSpy.count(), 1);
  1399. // 4.f) Repeat for autosaveDelay
  1400. config()->set(Config::AutoSaveAfterEveryChange, true);
  1401. QTest::mouseClick(entryNewWidget, Qt::LeftButton);
  1402. QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditEntryMode);
  1403. QTest::keyClicks(titleEdit, "Test autosaveDelay 4");
  1404. editEntryWidget->switchToPage(EditEntryWidget::Page::Main);
  1405. editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
  1406. QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
  1407. Tools::wait(150); // due to modify timer
  1408. m_db->metadata()->setAutosaveDelayMin(0);
  1409. // 4.g) Make sure changes are not saved
  1410. m_dbWidget->triggerAutosaveTimer();
  1411. QTRY_COMPARE(writeDbSignalSpy.count(), 1);
  1412. // 5 Cleanup
  1413. config()->set(Config::AutoSaveAfterEveryChange, false);
  1414. }
  1415. void TestGui::testDatabaseLocking()
  1416. {
  1417. QString origDbName = m_tabWidget->tabText(0);
  1418. MessageBox::setNextAnswer(MessageBox::Cancel);
  1419. triggerAction("actionLockAllDatabases");
  1420. QCOMPARE(m_tabWidget->tabText(0), origDbName + " [Locked]");
  1421. auto* actionDatabaseMerge = m_mainWindow->findChild<QAction*>("actionDatabaseMerge", Qt::FindChildrenRecursively);
  1422. QCOMPARE(actionDatabaseMerge->isEnabled(), false);
  1423. auto* actionDatabaseSave = m_mainWindow->findChild<QAction*>("actionDatabaseSave", Qt::FindChildrenRecursively);
  1424. QCOMPARE(actionDatabaseSave->isEnabled(), false);
  1425. DatabaseWidget* dbWidget = m_tabWidget->currentDatabaseWidget();
  1426. QVERIFY(dbWidget->isLocked());
  1427. auto* unlockDatabaseWidget = dbWidget->findChild<QWidget*>("databaseOpenWidget");
  1428. QWidget* editPassword =
  1429. unlockDatabaseWidget->findChild<PasswordWidget*>("editPassword")->findChild<QLineEdit*>("passwordEdit");
  1430. QVERIFY(editPassword);
  1431. QTest::keyClicks(editPassword, "a");
  1432. QTest::keyClick(editPassword, Qt::Key_Enter);
  1433. QVERIFY(!dbWidget->isLocked());
  1434. QCOMPARE(m_tabWidget->tabText(0), origDbName);
  1435. actionDatabaseMerge = m_mainWindow->findChild<QAction*>("actionDatabaseMerge", Qt::FindChildrenRecursively);
  1436. QCOMPARE(actionDatabaseMerge->isEnabled(), true);
  1437. }
  1438. void TestGui::testDragAndDropKdbxFiles()
  1439. {
  1440. const int openedDatabasesCount = m_tabWidget->count();
  1441. const QString badDatabaseFilePath(QString(KEEPASSX_TEST_DATA_DIR).append("/NotDatabase.notkdbx"));
  1442. const QString goodDatabaseFilePath(QString(KEEPASSX_TEST_DATA_DIR).append("/NewDatabase.kdbx"));
  1443. QMimeData badMimeData;
  1444. badMimeData.setUrls({QUrl::fromLocalFile(badDatabaseFilePath)});
  1445. QDragEnterEvent badDragEvent(QPoint(1, 1), Qt::LinkAction, &badMimeData, Qt::LeftButton, Qt::NoModifier);
  1446. qApp->notify(m_mainWindow.data(), &badDragEvent);
  1447. QCOMPARE(badDragEvent.isAccepted(), false);
  1448. QDropEvent badDropEvent(QPoint(1, 1), Qt::LinkAction, &badMimeData, Qt::LeftButton, Qt::NoModifier);
  1449. qApp->notify(m_mainWindow.data(), &badDropEvent);
  1450. QCOMPARE(badDropEvent.isAccepted(), false);
  1451. QCOMPARE(m_tabWidget->count(), openedDatabasesCount);
  1452. QMimeData goodMimeData;
  1453. goodMimeData.setUrls({QUrl::fromLocalFile(goodDatabaseFilePath)});
  1454. QDragEnterEvent goodDragEvent(QPoint(1, 1), Qt::LinkAction, &goodMimeData, Qt::LeftButton, Qt::NoModifier);
  1455. qApp->notify(m_mainWindow.data(), &goodDragEvent);
  1456. QCOMPARE(goodDragEvent.isAccepted(), true);
  1457. QDropEvent goodDropEvent(QPoint(1, 1), Qt::LinkAction, &goodMimeData, Qt::LeftButton, Qt::NoModifier);
  1458. qApp->notify(m_mainWindow.data(), &goodDropEvent);
  1459. QCOMPARE(goodDropEvent.isAccepted(), true);
  1460. QCOMPARE(m_tabWidget->count(), openedDatabasesCount + 1);
  1461. MessageBox::setNextAnswer(MessageBox::No);
  1462. triggerAction("actionDatabaseClose");
  1463. QTRY_COMPARE(m_tabWidget->count(), openedDatabasesCount);
  1464. }
  1465. void TestGui::testSortGroups()
  1466. {
  1467. auto* editGroupWidget = m_dbWidget->findChild<EditGroupWidget*>("editGroupWidget");
  1468. auto* nameEdit = editGroupWidget->findChild<QLineEdit*>("editName");
  1469. auto* editGroupWidgetButtonBox = editGroupWidget->findChild<QDialogButtonBox*>("buttonBox");
  1470. // Create some sub-groups
  1471. Group* rootGroup = m_db->rootGroup();
  1472. Group* internetGroup = rootGroup->findGroupByPath("Internet");
  1473. m_dbWidget->groupView()->setCurrentGroup(internetGroup);
  1474. m_dbWidget->createGroup();
  1475. QTest::keyClicks(nameEdit, "Google");
  1476. QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
  1477. m_dbWidget->groupView()->setCurrentGroup(internetGroup);
  1478. m_dbWidget->createGroup();
  1479. QTest::keyClicks(nameEdit, "eBay");
  1480. QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
  1481. m_dbWidget->groupView()->setCurrentGroup(internetGroup);
  1482. m_dbWidget->createGroup();
  1483. QTest::keyClicks(nameEdit, "Amazon");
  1484. QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
  1485. m_dbWidget->groupView()->setCurrentGroup(internetGroup);
  1486. m_dbWidget->createGroup();
  1487. QTest::keyClicks(nameEdit, "Facebook");
  1488. QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
  1489. m_dbWidget->groupView()->setCurrentGroup(rootGroup);
  1490. triggerAction("actionGroupSortAsc");
  1491. QList<Group*> children = rootGroup->children();
  1492. QCOMPARE(children[0]->name(), QString("eMail"));
  1493. QCOMPARE(children[1]->name(), QString("General"));
  1494. QCOMPARE(children[2]->name(), QString("Homebanking"));
  1495. QCOMPARE(children[3]->name(), QString("Internet"));
  1496. QCOMPARE(children[4]->name(), QString("Network"));
  1497. QCOMPARE(children[5]->name(), QString("Windows"));
  1498. QList<Group*> subChildren = internetGroup->children();
  1499. QCOMPARE(subChildren[0]->name(), QString("Amazon"));
  1500. QCOMPARE(subChildren[1]->name(), QString("eBay"));
  1501. QCOMPARE(subChildren[2]->name(), QString("Facebook"));
  1502. QCOMPARE(subChildren[3]->name(), QString("Google"));
  1503. triggerAction("actionGroupSortDesc");
  1504. children = rootGroup->children();
  1505. QCOMPARE(children[0]->name(), QString("Windows"));
  1506. QCOMPARE(children[1]->name(), QString("Network"));
  1507. QCOMPARE(children[2]->name(), QString("Internet"));
  1508. QCOMPARE(children[3]->name(), QString("Homebanking"));
  1509. QCOMPARE(children[4]->name(), QString("General"));
  1510. QCOMPARE(children[5]->name(), QString("eMail"));
  1511. subChildren = internetGroup->children();
  1512. QCOMPARE(subChildren[0]->name(), QString("Google"));
  1513. QCOMPARE(subChildren[1]->name(), QString("Facebook"));
  1514. QCOMPARE(subChildren[2]->name(), QString("eBay"));
  1515. QCOMPARE(subChildren[3]->name(), QString("Amazon"));
  1516. m_dbWidget->groupView()->setCurrentGroup(internetGroup);
  1517. triggerAction("actionGroupSortAsc");
  1518. children = rootGroup->children();
  1519. QCOMPARE(children[0]->name(), QString("Windows"));
  1520. QCOMPARE(children[1]->name(), QString("Network"));
  1521. QCOMPARE(children[2]->name(), QString("Internet"));
  1522. QCOMPARE(children[3]->name(), QString("Homebanking"));
  1523. QCOMPARE(children[4]->name(), QString("General"));
  1524. QCOMPARE(children[5]->name(), QString("eMail"));
  1525. subChildren = internetGroup->children();
  1526. QCOMPARE(subChildren[0]->name(), QString("Amazon"));
  1527. QCOMPARE(subChildren[1]->name(), QString("eBay"));
  1528. QCOMPARE(subChildren[2]->name(), QString("Facebook"));
  1529. QCOMPARE(subChildren[3]->name(), QString("Google"));
  1530. m_dbWidget->groupView()->setCurrentGroup(rootGroup);
  1531. triggerAction("actionGroupSortAsc");
  1532. m_dbWidget->groupView()->setCurrentGroup(internetGroup);
  1533. triggerAction("actionGroupSortDesc");
  1534. children = rootGroup->children();
  1535. QCOMPARE(children[0]->name(), QString("eMail"));
  1536. QCOMPARE(children[1]->name(), QString("General"));
  1537. QCOMPARE(children[2]->name(), QString("Homebanking"));
  1538. QCOMPARE(children[3]->name(), QString("Internet"));
  1539. QCOMPARE(children[4]->name(), QString("Network"));
  1540. QCOMPARE(children[5]->name(), QString("Windows"));
  1541. subChildren = internetGroup->children();
  1542. QCOMPARE(subChildren[0]->name(), QString("Google"));
  1543. QCOMPARE(subChildren[1]->name(), QString("Facebook"));
  1544. QCOMPARE(subChildren[2]->name(), QString("eBay"));
  1545. QCOMPARE(subChildren[3]->name(), QString("Amazon"));
  1546. }
  1547. void TestGui::testTrayRestoreHide()
  1548. {
  1549. if (!QSystemTrayIcon::isSystemTrayAvailable()) {
  1550. QSKIP("QSystemTrayIcon::isSystemTrayAvailable() = false, skipping tray restore/hide test…");
  1551. }
  1552. #ifndef Q_OS_MACOS
  1553. m_mainWindow->hideWindow();
  1554. QVERIFY(!m_mainWindow->isVisible());
  1555. auto* trayIcon = m_mainWindow->findChild<QSystemTrayIcon*>();
  1556. QVERIFY(trayIcon);
  1557. trayIcon->activated(QSystemTrayIcon::Trigger);
  1558. QTRY_VERIFY(m_mainWindow->isVisible());
  1559. trayIcon->activated(QSystemTrayIcon::Trigger);
  1560. QTRY_VERIFY(!m_mainWindow->isVisible());
  1561. trayIcon->activated(QSystemTrayIcon::MiddleClick);
  1562. QTRY_VERIFY(m_mainWindow->isVisible());
  1563. trayIcon->activated(QSystemTrayIcon::MiddleClick);
  1564. QTRY_VERIFY(!m_mainWindow->isVisible());
  1565. trayIcon->activated(QSystemTrayIcon::DoubleClick);
  1566. QTRY_VERIFY(m_mainWindow->isVisible());
  1567. trayIcon->activated(QSystemTrayIcon::DoubleClick);
  1568. QTRY_VERIFY(!m_mainWindow->isVisible());
  1569. // Ensure window is visible at the end
  1570. trayIcon->activated(QSystemTrayIcon::DoubleClick);
  1571. QTRY_VERIFY(m_mainWindow->isVisible());
  1572. #endif
  1573. }
  1574. void TestGui::testAutoType()
  1575. {
  1576. // Clear entries from root group to guarantee order
  1577. for (Entry* entry : m_db->rootGroup()->entries()) {
  1578. m_db->rootGroup()->removeEntry(entry);
  1579. }
  1580. Tools::wait(150);
  1581. // 1. Create an entry with Auto-Type disabled
  1582. // 1.a) Click the new entry button and set the title
  1583. auto* entryNewAction = m_mainWindow->findChild<QAction*>("actionEntryNew");
  1584. QVERIFY(entryNewAction->isEnabled());
  1585. auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
  1586. QVERIFY(toolBar);
  1587. QWidget* entryNewWidget = toolBar->widgetForAction(entryNewAction);
  1588. QVERIFY(entryNewWidget->isVisible());
  1589. QVERIFY(entryNewWidget->isEnabled());
  1590. QTest::mouseClick(entryNewWidget, Qt::LeftButton);
  1591. QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditEntryMode);
  1592. auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
  1593. QVERIFY(editEntryWidget);
  1594. auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
  1595. QVERIFY(titleEdit);
  1596. QTest::keyClicks(titleEdit, "1. Entry With Disabled Auto-Type");
  1597. auto* usernameComboBox = editEntryWidget->findChild<QComboBox*>("usernameComboBox");
  1598. QVERIFY(usernameComboBox);
  1599. QTest::mouseClick(usernameComboBox, Qt::LeftButton);
  1600. QTest::keyClicks(usernameComboBox, "AutocompletionUsername");
  1601. // 1.b) Uncheck Auto-Type checkbox
  1602. editEntryWidget->switchToPage(EditEntryWidget::Page::AutoType);
  1603. auto* enableAutoTypeButton = editEntryWidget->findChild<QCheckBox*>("enableButton");
  1604. QVERIFY(enableAutoTypeButton);
  1605. QVERIFY(enableAutoTypeButton->isVisible());
  1606. QVERIFY(enableAutoTypeButton->isEnabled());
  1607. enableAutoTypeButton->click();
  1608. QVERIFY(!enableAutoTypeButton->isChecked());
  1609. // 1.c) Save changes
  1610. editEntryWidget->switchToPage(EditEntryWidget::Page::Main);
  1611. auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
  1612. QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
  1613. // 2. Create an entry with default/inherited Auto-Type sequence
  1614. // 2.a) Click the new entry button and set the title
  1615. QTest::mouseClick(entryNewWidget, Qt::LeftButton);
  1616. QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditEntryMode);
  1617. QTest::keyClicks(titleEdit, "2. Entry With Default Auto-Type Sequence");
  1618. QTest::mouseClick(usernameComboBox, Qt::LeftButton);
  1619. QTest::keyClicks(usernameComboBox, "AutocompletionUsername");
  1620. // 2.b) Confirm AutoType is enabled and default
  1621. editEntryWidget->switchToPage(EditEntryWidget::Page::AutoType);
  1622. QVERIFY(enableAutoTypeButton->isChecked());
  1623. auto* inheritSequenceButton = editEntryWidget->findChild<QRadioButton*>("inheritSequenceButton");
  1624. QVERIFY(inheritSequenceButton->isChecked());
  1625. // 2.c) Save changes
  1626. editEntryWidget->switchToPage(EditEntryWidget::Page::Main);
  1627. QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
  1628. // 3. Create an entry with custom Auto-Type sequence
  1629. // 3.a) Click the new entry button and set the title
  1630. QTest::mouseClick(entryNewWidget, Qt::LeftButton);
  1631. QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditEntryMode);
  1632. QTest::keyClicks(titleEdit, "3. Entry With Custom Auto-Type Sequence");
  1633. QTest::mouseClick(usernameComboBox, Qt::LeftButton);
  1634. QTest::keyClicks(usernameComboBox, "AutocompletionUsername");
  1635. // 3.b) Confirm AutoType is enabled and set custom sequence
  1636. editEntryWidget->switchToPage(EditEntryWidget::Page::AutoType);
  1637. QVERIFY(enableAutoTypeButton->isChecked());
  1638. auto* customSequenceButton = editEntryWidget->findChild<QRadioButton*>("customSequenceButton");
  1639. QTest::mouseClick(customSequenceButton, Qt::LeftButton);
  1640. QVERIFY(customSequenceButton->isChecked());
  1641. QVERIFY(!inheritSequenceButton->isChecked());
  1642. auto* sequenceEdit = editEntryWidget->findChild<QLineEdit*>("sequenceEdit");
  1643. QVERIFY(sequenceEdit);
  1644. sequenceEdit->setFocus();
  1645. QTRY_VERIFY(sequenceEdit->hasFocus());
  1646. QTest::keyClicks(sequenceEdit, "{USERNAME}{TAB}{TAB}{PASSWORD}{ENTER}");
  1647. // 3.c) Save changes
  1648. editEntryWidget->switchToPage(EditEntryWidget::Page::Main);
  1649. QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
  1650. QApplication::processEvents();
  1651. // Check total number of entries matches expected
  1652. auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
  1653. QVERIFY(entryView);
  1654. QTRY_COMPARE(entryView->model()->rowCount(), 3);
  1655. // Sort entries by title
  1656. entryView->sortByColumn(1, Qt::AscendingOrder);
  1657. // Select first entry
  1658. entryView->selectionModel()->clearSelection();
  1659. QModelIndex entryIndex = entryView->model()->index(0, 0);
  1660. entryView->selectionModel()->select(entryIndex, QItemSelectionModel::Rows | QItemSelectionModel::Select);
  1661. auto* entryPreviewWidget = m_dbWidget->findChild<EntryPreviewWidget*>("previewWidget");
  1662. QVERIFY(entryPreviewWidget->isVisible());
  1663. // Check that the Autotype tab in entry preview pane is disabled for entry with disabled Auto-Type
  1664. auto* entryAutotypeTab = entryPreviewWidget->findChild<QWidget*>("entryAutotypeTab");
  1665. QVERIFY(!entryAutotypeTab->isEnabled());
  1666. // Check that Auto-Type is disabled in the actual entry model as well
  1667. Entry* entry = entryView->entryFromIndex(entryIndex);
  1668. QVERIFY(!entry->autoTypeEnabled());
  1669. // Select second entry
  1670. entryView->selectionModel()->clearSelection();
  1671. entryIndex = entryView->model()->index(1, 0);
  1672. entryView->selectionModel()->select(entryIndex, QItemSelectionModel::Rows | QItemSelectionModel::Select);
  1673. QVERIFY(entryPreviewWidget->isVisible());
  1674. // Check that the Autotype tab in entry preview pane is enabled for entry with default Auto-Type sequence;
  1675. QVERIFY(entryAutotypeTab->isEnabled());
  1676. // Check that Auto-Type is enabled in the actual entry model as well
  1677. entry = entryView->entryFromIndex(entryIndex);
  1678. QVERIFY(entry->autoTypeEnabled());
  1679. // Select third entry
  1680. entryView->selectionModel()->clearSelection();
  1681. entryIndex = entryView->model()->index(2, 0);
  1682. entryView->selectionModel()->select(entryIndex, QItemSelectionModel::Rows | QItemSelectionModel::Select);
  1683. QVERIFY(entryPreviewWidget->isVisible());
  1684. // Check that the Autotype tab in entry preview pane is enabled for entry with custom Auto-Type sequence
  1685. QVERIFY(entryAutotypeTab->isEnabled());
  1686. // Check that Auto-Type is enabled in the actual entry model as well
  1687. entry = entryView->entryFromIndex(entryIndex);
  1688. QVERIFY(entry->autoTypeEnabled());
  1689. // De-select third entry
  1690. entryView->selectionModel()->clearSelection();
  1691. }
  1692. void TestGui::testMenuActionStates()
  1693. {
  1694. auto isActionEnabled = [this](const QString& actionName) -> bool {
  1695. auto action = m_mainWindow->findChild<QAction*>(actionName);
  1696. if (!action) {
  1697. QTest::qFail(qPrintable(QString("Invalid action specified: %1").arg(actionName)), __FILE__, __LINE__);
  1698. return false;
  1699. }
  1700. return action->isEnabled();
  1701. };
  1702. // Start with database open and unlocked
  1703. qInfo("Actions Test: Database open and unlocked");
  1704. QVERIFY(isActionEnabled("actionEntryNew"));
  1705. QVERIFY(isActionEnabled("actionGroupNew"));
  1706. QVERIFY(isActionEnabled("actionDatabaseSaveAs"));
  1707. QVERIFY(isActionEnabled("actionDatabaseClose"));
  1708. QVERIFY(isActionEnabled("actionDatabaseMerge"));
  1709. QVERIFY(isActionEnabled("actionDatabaseSettings"));
  1710. QVERIFY(isActionEnabled("actionReports"));
  1711. QVERIFY(isActionEnabled("actionLockDatabase"));
  1712. QVERIFY(isActionEnabled("actionLockAllDatabases"));
  1713. QVERIFY(isActionEnabled("actionImport"));
  1714. QVERIFY(isActionEnabled("actionExportCsv"));
  1715. QVERIFY(isActionEnabled("actionSettings"));
  1716. QVERIFY(isActionEnabled("actionPasswordGenerator"));
  1717. // Edit entry actions
  1718. qInfo("Actions Test: Editing an entry");
  1719. triggerAction("actionEntryEdit");
  1720. QVERIFY(!isActionEnabled("actionEntryNew"));
  1721. QVERIFY(isActionEnabled("actionEntryCopyUsername"));
  1722. QVERIFY(!isActionEnabled("actionEntrySetupTotp"));
  1723. QVERIFY(!isActionEnabled("actionGroupNew"));
  1724. QVERIFY(isActionEnabled("actionDatabaseSaveAs"));
  1725. QVERIFY(isActionEnabled("actionDatabaseClose"));
  1726. QVERIFY(!isActionEnabled("actionDatabaseMerge"));
  1727. QVERIFY(!isActionEnabled("actionDatabaseSettings"));
  1728. QVERIFY(!isActionEnabled("actionReports"));
  1729. QVERIFY(isActionEnabled("actionLockDatabase"));
  1730. QVERIFY(isActionEnabled("actionLockAllDatabases"));
  1731. QVERIFY(isActionEnabled("actionSettings"));
  1732. QVERIFY(isActionEnabled("actionPasswordGenerator"));
  1733. // Special Case - Recycle Bin
  1734. qInfo("Actions Test: Special case - Recycle Bin");
  1735. m_dbWidget->switchToMainView();
  1736. QApplication::processEvents();
  1737. QVERIFY(m_db->metadata()->recycleBinEnabled());
  1738. triggerAction("actionEntryDelete");
  1739. m_dbWidget->groupView()->setCurrentGroup(m_db->metadata()->recycleBin());
  1740. QVERIFY(m_dbWidget->isRecycleBinSelected());
  1741. QVERIFY(isActionEnabled("actionEntryRestore"));
  1742. QVERIFY(isActionEnabled("actionGroupEmptyRecycleBin"));
  1743. QVERIFY(!isActionEnabled("actionEntryNew"));
  1744. QVERIFY(!isActionEnabled("actionEntryClone"));
  1745. QVERIFY(!isActionEnabled("actionGroupNew"));
  1746. QVERIFY(!isActionEnabled("actionGroupClone"));
  1747. // Database Settings
  1748. qInfo("Actions Test: Database settings");
  1749. triggerAction("actionDatabaseSettings");
  1750. QVERIFY(!isActionEnabled("actionEntryNew"));
  1751. QVERIFY(!isActionEnabled("actionEntrySetupTotp"));
  1752. QVERIFY(!isActionEnabled("actionGroupNew"));
  1753. QVERIFY(isActionEnabled("actionDatabaseSaveAs"));
  1754. QVERIFY(isActionEnabled("actionDatabaseClose"));
  1755. QVERIFY(!isActionEnabled("actionDatabaseMerge"));
  1756. QVERIFY(isActionEnabled("actionDatabaseSettings"));
  1757. QVERIFY(isActionEnabled("actionDatabaseSecurity"));
  1758. QVERIFY(!isActionEnabled("actionReports"));
  1759. QVERIFY(isActionEnabled("actionLockDatabase"));
  1760. QVERIFY(isActionEnabled("actionSettings"));
  1761. QVERIFY(isActionEnabled("actionPasswordGenerator"));
  1762. // Database Reports
  1763. qInfo("Actions Test: Database reports");
  1764. triggerAction("actionDatabaseSettings");
  1765. triggerAction("actionReports");
  1766. QVERIFY(!isActionEnabled("actionEntryNew"));
  1767. QVERIFY(!isActionEnabled("actionEntrySetupTotp"));
  1768. QVERIFY(!isActionEnabled("actionGroupNew"));
  1769. QVERIFY(isActionEnabled("actionDatabaseSaveAs"));
  1770. QVERIFY(isActionEnabled("actionDatabaseClose"));
  1771. QVERIFY(!isActionEnabled("actionDatabaseMerge"));
  1772. QVERIFY(!isActionEnabled("actionDatabaseSettings"));
  1773. QVERIFY(!isActionEnabled("actionDatabaseSecurity"));
  1774. QVERIFY(isActionEnabled("actionReports"));
  1775. QVERIFY(isActionEnabled("actionLockDatabase"));
  1776. QVERIFY(isActionEnabled("actionSettings"));
  1777. QVERIFY(isActionEnabled("actionPasswordGenerator"));
  1778. // Application Settings
  1779. qInfo("Actions Test: Application settings");
  1780. triggerAction("actionSettings");
  1781. QVERIFY(!isActionEnabled("actionDatabaseSettings"));
  1782. QVERIFY(!isActionEnabled("actionDatabaseSecurity"));
  1783. QVERIFY(!isActionEnabled("actionReports"));
  1784. QVERIFY(isActionEnabled("actionSettings"));
  1785. QVERIFY(isActionEnabled("actionPasswordGenerator"));
  1786. // Locked Database
  1787. qInfo("Actions Test: Database locked");
  1788. triggerAction("actionSettings");
  1789. MessageBox::setNextAnswer(MessageBox::Discard);
  1790. triggerAction("actionLockDatabase");
  1791. QVERIFY(!isActionEnabled("actionEntryNew"));
  1792. QVERIFY(!isActionEnabled("actionGroupNew"));
  1793. QVERIFY(!isActionEnabled("actionDatabaseSaveAs"));
  1794. QVERIFY(isActionEnabled("actionDatabaseClose"));
  1795. QVERIFY(!isActionEnabled("actionDatabaseMerge"));
  1796. QVERIFY(!isActionEnabled("actionDatabaseSettings"));
  1797. QVERIFY(!isActionEnabled("actionReports"));
  1798. QVERIFY(!isActionEnabled("actionLockDatabase"));
  1799. QVERIFY(!isActionEnabled("actionLockAllDatabases"));
  1800. QVERIFY(isActionEnabled("actionSettings"));
  1801. QVERIFY(isActionEnabled("actionPasswordGenerator"));
  1802. // Welcome Screen
  1803. qInfo("Actions Test: Welcome screen");
  1804. triggerAction("actionDatabaseClose");
  1805. QVERIFY(!isActionEnabled("actionEntryNew"));
  1806. QVERIFY(!isActionEnabled("actionGroupNew"));
  1807. QVERIFY(!isActionEnabled("actionDatabaseSaveAs"));
  1808. QVERIFY(!isActionEnabled("actionDatabaseClose"));
  1809. QVERIFY(!isActionEnabled("actionDatabaseMerge"));
  1810. QVERIFY(!isActionEnabled("actionDatabaseSettings"));
  1811. QVERIFY(!isActionEnabled("actionReports"));
  1812. QVERIFY(!isActionEnabled("actionLockDatabase"));
  1813. QVERIFY(!isActionEnabled("actionLockAllDatabases"));
  1814. QVERIFY(isActionEnabled("actionImport"));
  1815. QVERIFY(isActionEnabled("actionSettings"));
  1816. QVERIFY(isActionEnabled("actionPasswordGenerator"));
  1817. }
  1818. void TestGui::addCannedEntries()
  1819. {
  1820. // Find buttons
  1821. auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
  1822. QWidget* entryNewWidget = toolBar->widgetForAction(m_mainWindow->findChild<QAction*>("actionEntryNew"));
  1823. auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
  1824. auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
  1825. auto* passwordEdit =
  1826. editEntryWidget->findChild<PasswordWidget*>("passwordEdit")->findChild<QLineEdit*>("passwordEdit");
  1827. // Add entry "test" and confirm added
  1828. QTest::mouseClick(entryNewWidget, Qt::LeftButton);
  1829. QTest::keyClicks(titleEdit, "test");
  1830. auto* editEntryWidgetTagsEdit = editEntryWidget->findChild<TagsEdit*>("tagsList");
  1831. editEntryWidgetTagsEdit->tags(QStringList() << "testTag");
  1832. auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
  1833. QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
  1834. // Add entry "something 2"
  1835. QTest::mouseClick(entryNewWidget, Qt::LeftButton);
  1836. QTest::keyClicks(titleEdit, "something 2");
  1837. QTest::keyClicks(passwordEdit, "something 2");
  1838. QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
  1839. // Add entry "something 3"
  1840. QTest::mouseClick(entryNewWidget, Qt::LeftButton);
  1841. QTest::keyClicks(titleEdit, "something 3");
  1842. QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
  1843. }
  1844. void TestGui::checkDatabase(const QString& filePath, const QString& expectedDbName)
  1845. {
  1846. auto key = QSharedPointer<CompositeKey>::create();
  1847. key->addKey(QSharedPointer<PasswordKey>::create("a"));
  1848. auto dbSaved = QSharedPointer<Database>::create();
  1849. QVERIFY(dbSaved->open(filePath, key, nullptr));
  1850. QCOMPARE(dbSaved->metadata()->name(), expectedDbName);
  1851. }
  1852. void TestGui::checkDatabase(const QString& filePath)
  1853. {
  1854. checkDatabase(filePath.isEmpty() ? m_dbFilePath : filePath, m_db->metadata()->name());
  1855. }
  1856. void TestGui::checkSaveDatabase()
  1857. {
  1858. // Attempt to save the database up to two times to overcome transient file errors
  1859. QTRY_VERIFY(m_db->isModified());
  1860. QTRY_VERIFY(m_tabWidget->tabText(m_tabWidget->currentIndex()).endsWith("*"));
  1861. int i = 0;
  1862. do {
  1863. triggerAction("actionDatabaseSave");
  1864. if (!m_db->isModified()) {
  1865. checkDatabase();
  1866. return;
  1867. }
  1868. QWARN("Failed to save database, trying again...");
  1869. Tools::wait(250);
  1870. } while (++i < 2);
  1871. QFAIL("Could not save database.");
  1872. }
  1873. void TestGui::checkStatusBarText(const QString& textFragment)
  1874. {
  1875. QApplication::processEvents();
  1876. QVERIFY(m_statusBarLabel->isVisible());
  1877. QTRY_VERIFY2(m_statusBarLabel->text().startsWith(textFragment),
  1878. qPrintable(QString("'%1' doesn't start with '%2'").arg(m_statusBarLabel->text(), textFragment)));
  1879. }
  1880. void TestGui::triggerAction(const QString& name)
  1881. {
  1882. auto* action = m_mainWindow->findChild<QAction*>(name);
  1883. QVERIFY2(action, qPrintable(QString("Action doesn't exist: %1").arg(name)));
  1884. QVERIFY2(action->isEnabled(), qPrintable(QString("Action is disabled: %1").arg(name)));
  1885. action->trigger();
  1886. QApplication::processEvents();
  1887. }
  1888. void TestGui::dragAndDropGroup(const QModelIndex& sourceIndex,
  1889. const QModelIndex& targetIndex,
  1890. int row,
  1891. bool expectedResult,
  1892. const QString& expectedParentName,
  1893. int expectedPos)
  1894. {
  1895. QVERIFY(sourceIndex.isValid());
  1896. QVERIFY(targetIndex.isValid());
  1897. auto groupModel = qobject_cast<GroupModel*>(m_dbWidget->findChild<GroupView*>("groupView")->model());
  1898. QMimeData mimeData;
  1899. QByteArray encoded;
  1900. QDataStream stream(&encoded, QIODevice::WriteOnly);
  1901. Group* group = groupModel->groupFromIndex(sourceIndex);
  1902. stream << group->database()->uuid() << group->uuid();
  1903. mimeData.setData("application/x-keepassx-group", encoded);
  1904. QCOMPARE(groupModel->dropMimeData(&mimeData, Qt::MoveAction, row, 0, targetIndex), expectedResult);
  1905. QCOMPARE(group->parentGroup()->name(), expectedParentName);
  1906. QCOMPARE(group->parentGroup()->children().indexOf(group), expectedPos);
  1907. }
  1908. void TestGui::clickIndex(const QModelIndex& index,
  1909. QAbstractItemView* view,
  1910. Qt::MouseButton button,
  1911. Qt::KeyboardModifiers stateKey)
  1912. {
  1913. view->scrollTo(index);
  1914. QTest::mouseClick(view->viewport(), button, stateKey, view->visualRect(index).center());
  1915. }