TestCli.cpp 73 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954
  1. /*
  2. * Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
  3. *
  4. * This program is free software: you can redistribute it and/or modify
  5. * it under the terms of the GNU General Public License as published by
  6. * the Free Software Foundation, either version 2 or (at your option)
  7. * version 3 of the License.
  8. *
  9. * This program is distributed in the hope that it will be useful,
  10. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. * GNU General Public License for more details.
  13. *
  14. * You should have received a copy of the GNU General Public License
  15. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. */
  17. #include "TestCli.h"
  18. #include "config-keepassx-tests.h"
  19. #include "core/Bootstrap.h"
  20. #include "core/Config.h"
  21. #include "core/Entry.h"
  22. #include "core/Group.h"
  23. #include "core/Metadata.h"
  24. #include "core/Tools.h"
  25. #include "crypto/Crypto.h"
  26. #include "keys/FileKey.h"
  27. #include "keys/drivers/YubiKey.h"
  28. #include "cli/Add.h"
  29. #include "cli/AddGroup.h"
  30. #include "cli/Analyze.h"
  31. #include "cli/Clip.h"
  32. #include "cli/Create.h"
  33. #include "cli/Diceware.h"
  34. #include "cli/Edit.h"
  35. #include "cli/Estimate.h"
  36. #include "cli/Export.h"
  37. #include "cli/Generate.h"
  38. #include "cli/Help.h"
  39. #include "cli/Import.h"
  40. #include "cli/Info.h"
  41. #include "cli/List.h"
  42. #include "cli/Locate.h"
  43. #include "cli/Merge.h"
  44. #include "cli/Move.h"
  45. #include "cli/Open.h"
  46. #include "cli/Remove.h"
  47. #include "cli/RemoveGroup.h"
  48. #include "cli/Show.h"
  49. #include "cli/Utils.h"
  50. #include <QClipboard>
  51. #include <QSignalSpy>
  52. #include <QtConcurrent>
  53. QTEST_MAIN(TestCli)
  54. void TestCli::initTestCase()
  55. {
  56. QVERIFY(Crypto::init());
  57. Config::createTempFileInstance();
  58. Bootstrap::bootstrap();
  59. m_devNull.reset(new QFile());
  60. #ifdef Q_OS_WIN
  61. m_devNull->open(fopen("nul", "w"), QIODevice::WriteOnly);
  62. #else
  63. m_devNull->open(fopen("/dev/null", "w"), QIODevice::WriteOnly);
  64. #endif
  65. Utils::DEVNULL.setDevice(m_devNull.data());
  66. }
  67. void TestCli::init()
  68. {
  69. const auto file = QString(KEEPASSX_TEST_DATA_DIR).append("/%1");
  70. m_dbFile.reset(new TemporaryFile());
  71. m_dbFile->copyFromFile(file.arg("NewDatabase.kdbx"));
  72. m_dbFile2.reset(new TemporaryFile());
  73. m_dbFile2->copyFromFile(file.arg("NewDatabase2.kdbx"));
  74. m_dbFileMulti.reset(new TemporaryFile());
  75. m_dbFileMulti->copyFromFile(file.arg("NewDatabaseMulti.kdbx"));
  76. m_xmlFile.reset(new TemporaryFile());
  77. m_xmlFile->copyFromFile(file.arg("NewDatabase.xml"));
  78. m_keyFileProtectedDbFile.reset(new TemporaryFile());
  79. m_keyFileProtectedDbFile->copyFromFile(file.arg("KeyFileProtected.kdbx"));
  80. m_keyFileProtectedNoPasswordDbFile.reset(new TemporaryFile());
  81. m_keyFileProtectedNoPasswordDbFile->copyFromFile(file.arg("KeyFileProtectedNoPassword.kdbx"));
  82. m_yubiKeyProtectedDbFile.reset(new TemporaryFile());
  83. m_yubiKeyProtectedDbFile->copyFromFile(file.arg("YubiKeyProtectedPasswords.kdbx"));
  84. m_stdout.reset(new QBuffer());
  85. m_stdout->open(QIODevice::ReadWrite);
  86. Utils::STDOUT.setDevice(m_stdout.data());
  87. m_stderr.reset(new QBuffer());
  88. m_stderr->open(QIODevice::ReadWrite);
  89. Utils::STDERR.setDevice(m_stderr.data());
  90. m_stdin.reset(new QBuffer());
  91. m_stdin->open(QIODevice::ReadWrite);
  92. Utils::STDIN.setDevice(m_stdin.data());
  93. }
  94. void TestCli::cleanup()
  95. {
  96. m_dbFile.reset();
  97. m_dbFile2.reset();
  98. m_dbFileMulti.reset();
  99. m_keyFileProtectedDbFile.reset();
  100. m_keyFileProtectedNoPasswordDbFile.reset();
  101. m_yubiKeyProtectedDbFile.reset();
  102. Utils::STDOUT.setDevice(nullptr);
  103. Utils::STDERR.setDevice(nullptr);
  104. Utils::STDIN.setDevice(nullptr);
  105. }
  106. void TestCli::cleanupTestCase()
  107. {
  108. m_devNull.reset();
  109. }
  110. QSharedPointer<Database> TestCli::readDatabase(const QString& filename, const QString& pw, const QString& keyfile)
  111. {
  112. auto db = QSharedPointer<Database>::create();
  113. auto key = QSharedPointer<CompositeKey>::create();
  114. if (filename.isEmpty()) {
  115. // Open the default test database
  116. key->addKey(QSharedPointer<PasswordKey>::create("a"));
  117. if (!db->open(m_dbFile->fileName(), key)) {
  118. return {};
  119. }
  120. } else {
  121. // Open the specified database file using supplied credentials
  122. key->addKey(QSharedPointer<PasswordKey>::create(pw));
  123. if (!keyfile.isEmpty()) {
  124. auto filekey = QSharedPointer<FileKey>::create();
  125. filekey->load(keyfile);
  126. key->addKey(filekey);
  127. }
  128. if (!db->open(filename, key)) {
  129. return {};
  130. }
  131. }
  132. return db;
  133. }
  134. int TestCli::execCmd(Command& cmd, const QStringList& args) const
  135. {
  136. // Move to end of stream
  137. m_stdout->readAll();
  138. m_stderr->readAll();
  139. // Record stream position
  140. auto outPos = m_stdout->pos();
  141. auto errPos = m_stderr->pos();
  142. // Execute command
  143. int ret = cmd.execute(args);
  144. // Move back to recorded position
  145. m_stdout->seek(outPos);
  146. m_stderr->seek(errPos);
  147. // Skip over blank lines
  148. QByteArray newline("\n");
  149. while (m_stdout->peek(1) == newline) {
  150. m_stdout->readLine();
  151. }
  152. while (m_stderr->peek(1) == newline) {
  153. m_stdout->readLine();
  154. }
  155. return ret;
  156. }
  157. bool TestCli::isTotp(const QString& value)
  158. {
  159. static const QRegularExpression totp("^\\d{6}$");
  160. return totp.match(value.trimmed()).hasMatch();
  161. }
  162. void TestCli::setInput(const QString& input)
  163. {
  164. setInput(QStringList(input));
  165. }
  166. void TestCli::setInput(const QStringList& input)
  167. {
  168. auto ba = input.join("\n").toLatin1();
  169. // Always end in newline
  170. if (!ba.endsWith("\n")) {
  171. ba.append("\n");
  172. }
  173. auto pos = m_stdin->pos();
  174. m_stdin->write(ba);
  175. m_stdin->seek(pos);
  176. }
  177. void TestCli::testBatchCommands()
  178. {
  179. Commands::setupCommands(false);
  180. QVERIFY(Commands::getCommand("add"));
  181. QVERIFY(Commands::getCommand("analyze"));
  182. QVERIFY(Commands::getCommand("clip"));
  183. QVERIFY(Commands::getCommand("close"));
  184. QVERIFY(Commands::getCommand("db-create"));
  185. QVERIFY(Commands::getCommand("db-info"));
  186. QVERIFY(Commands::getCommand("diceware"));
  187. QVERIFY(Commands::getCommand("edit"));
  188. QVERIFY(Commands::getCommand("estimate"));
  189. QVERIFY(Commands::getCommand("export"));
  190. QVERIFY(Commands::getCommand("generate"));
  191. QVERIFY(Commands::getCommand("help"));
  192. QVERIFY(Commands::getCommand("import"));
  193. QVERIFY(Commands::getCommand("locate"));
  194. QVERIFY(Commands::getCommand("ls"));
  195. QVERIFY(Commands::getCommand("merge"));
  196. QVERIFY(Commands::getCommand("mkdir"));
  197. QVERIFY(Commands::getCommand("mv"));
  198. QVERIFY(Commands::getCommand("open"));
  199. QVERIFY(Commands::getCommand("rm"));
  200. QVERIFY(Commands::getCommand("rmdir"));
  201. QVERIFY(Commands::getCommand("show"));
  202. QVERIFY(!Commands::getCommand("doesnotexist"));
  203. QCOMPARE(Commands::getCommands().size(), 22);
  204. }
  205. void TestCli::testInteractiveCommands()
  206. {
  207. Commands::setupCommands(true);
  208. QVERIFY(Commands::getCommand("add"));
  209. QVERIFY(Commands::getCommand("analyze"));
  210. QVERIFY(Commands::getCommand("clip"));
  211. QVERIFY(Commands::getCommand("close"));
  212. QVERIFY(Commands::getCommand("db-create"));
  213. QVERIFY(Commands::getCommand("db-info"));
  214. QVERIFY(Commands::getCommand("diceware"));
  215. QVERIFY(Commands::getCommand("edit"));
  216. QVERIFY(Commands::getCommand("estimate"));
  217. QVERIFY(Commands::getCommand("exit"));
  218. QVERIFY(Commands::getCommand("generate"));
  219. QVERIFY(Commands::getCommand("help"));
  220. QVERIFY(Commands::getCommand("locate"));
  221. QVERIFY(Commands::getCommand("ls"));
  222. QVERIFY(Commands::getCommand("merge"));
  223. QVERIFY(Commands::getCommand("mkdir"));
  224. QVERIFY(Commands::getCommand("mv"));
  225. QVERIFY(Commands::getCommand("open"));
  226. QVERIFY(Commands::getCommand("quit"));
  227. QVERIFY(Commands::getCommand("rm"));
  228. QVERIFY(Commands::getCommand("rmdir"));
  229. QVERIFY(Commands::getCommand("show"));
  230. QVERIFY(!Commands::getCommand("doesnotexist"));
  231. QCOMPARE(Commands::getCommands().size(), 22);
  232. }
  233. void TestCli::testAdd()
  234. {
  235. Add addCmd;
  236. QVERIFY(!addCmd.name.isEmpty());
  237. QVERIFY(addCmd.getDescriptionLine().contains(addCmd.name));
  238. setInput("a");
  239. execCmd(addCmd,
  240. {"add",
  241. "-u",
  242. "newuser",
  243. "--url",
  244. "https://example.com/",
  245. "-g",
  246. "-L",
  247. "20",
  248. "--notes",
  249. "some notes",
  250. m_dbFile->fileName(),
  251. "/newuser-entry"});
  252. m_stderr->readLine(); // Skip password prompt
  253. QCOMPARE(m_stderr->readAll(), QByteArray());
  254. QVERIFY(m_stdout->readAll().contains("Successfully added entry newuser-entry."));
  255. auto db = readDatabase();
  256. auto* entry = db->rootGroup()->findEntryByPath("/newuser-entry");
  257. QVERIFY(entry);
  258. QCOMPARE(entry->username(), QString("newuser"));
  259. QCOMPARE(entry->url(), QString("https://example.com/"));
  260. QCOMPARE(entry->password().size(), 20);
  261. QCOMPARE(entry->notes(), QString("some notes"));
  262. // Quiet option
  263. setInput("a");
  264. execCmd(addCmd, {"add", "-q", "-u", "newuser", "-g", "-L", "20", m_dbFile->fileName(), "/newentry-quiet"});
  265. QCOMPARE(m_stderr->readAll(), QByteArray());
  266. QCOMPARE(m_stdout->readAll(), QByteArray());
  267. db = readDatabase();
  268. entry = db->rootGroup()->findEntryByPath("/newentry-quiet");
  269. QVERIFY(entry);
  270. QCOMPARE(entry->password().size(), 20);
  271. setInput({"a", "newpassword"});
  272. execCmd(addCmd,
  273. {"add", "-u", "newuser2", "--url", "https://example.net/", "-p", m_dbFile->fileName(), "/newuser-entry2"});
  274. QVERIFY(m_stdout->readAll().contains("Successfully added entry newuser-entry2."));
  275. db = readDatabase();
  276. entry = db->rootGroup()->findEntryByPath("/newuser-entry2");
  277. QVERIFY(entry);
  278. QCOMPARE(entry->username(), QString("newuser2"));
  279. QCOMPARE(entry->url(), QString("https://example.net/"));
  280. QCOMPARE(entry->password(), QString("newpassword"));
  281. // Password generation options
  282. setInput("a");
  283. execCmd(addCmd, {"add", "-u", "newuser3", "-g", "-L", "34", m_dbFile->fileName(), "/newuser-entry3"});
  284. QVERIFY(m_stdout->readAll().contains("Successfully added entry newuser-entry3."));
  285. db = readDatabase();
  286. entry = db->rootGroup()->findEntryByPath("/newuser-entry3");
  287. QVERIFY(entry);
  288. QCOMPARE(entry->username(), QString("newuser3"));
  289. QCOMPARE(entry->password().size(), 34);
  290. QRegularExpression defaultPasswordClassesRegex("^[a-zA-Z0-9]+$");
  291. QVERIFY(defaultPasswordClassesRegex.match(entry->password()).hasMatch());
  292. setInput("a");
  293. execCmd(addCmd,
  294. {"add",
  295. "-u",
  296. "newuser4",
  297. "-g",
  298. "-L",
  299. "20",
  300. "--every-group",
  301. "-s",
  302. "-n",
  303. "-U",
  304. "-l",
  305. m_dbFile->fileName(),
  306. "/newuser-entry4"});
  307. QVERIFY(m_stdout->readAll().contains("Successfully added entry newuser-entry4."));
  308. db = readDatabase();
  309. entry = db->rootGroup()->findEntryByPath("/newuser-entry4");
  310. QVERIFY(entry);
  311. QCOMPARE(entry->username(), QString("newuser4"));
  312. QCOMPARE(entry->password().size(), 20);
  313. QVERIFY(!defaultPasswordClassesRegex.match(entry->password()).hasMatch());
  314. setInput("a");
  315. execCmd(addCmd, {"add", "-u", "newuser5", "--notes", "test\\nnew line", m_dbFile->fileName(), "/newuser-entry5"});
  316. m_stderr->readLine(); // skip password prompt
  317. QCOMPARE(m_stderr->readAll(), QByteArray(""));
  318. QCOMPARE(m_stdout->readAll(), QByteArray("Successfully added entry newuser-entry5.\n"));
  319. db = readDatabase();
  320. entry = db->rootGroup()->findEntryByPath("/newuser-entry5");
  321. QVERIFY(entry);
  322. QCOMPARE(entry->username(), QString("newuser5"));
  323. QCOMPARE(entry->notes(), QString("test\nnew line"));
  324. }
  325. void TestCli::testAddGroup()
  326. {
  327. AddGroup addGroupCmd;
  328. QVERIFY(!addGroupCmd.name.isEmpty());
  329. QVERIFY(addGroupCmd.getDescriptionLine().contains(addGroupCmd.name));
  330. setInput("a");
  331. execCmd(addGroupCmd, {"mkdir", m_dbFile->fileName(), "/new_group"});
  332. m_stderr->readLine(); // Skip password prompt
  333. QCOMPARE(m_stderr->readAll(), QByteArray());
  334. QCOMPARE(m_stdout->readAll(), QByteArray("Successfully added group new_group.\n"));
  335. auto db = readDatabase();
  336. auto* group = db->rootGroup()->findGroupByPath("new_group");
  337. QVERIFY(group);
  338. QCOMPARE(group->name(), QString("new_group"));
  339. // Trying to add the same group should fail.
  340. setInput("a");
  341. execCmd(addGroupCmd, {"mkdir", m_dbFile->fileName(), "/new_group"});
  342. QVERIFY(m_stderr->readAll().contains("Group /new_group already exists!"));
  343. QCOMPARE(m_stdout->readAll(), QByteArray());
  344. // Should be able to add groups down the tree.
  345. setInput("a");
  346. execCmd(addGroupCmd, {"mkdir", m_dbFile->fileName(), "/new_group/newer_group"});
  347. QVERIFY(m_stdout->readAll().contains("Successfully added group newer_group."));
  348. db = readDatabase();
  349. group = db->rootGroup()->findGroupByPath("new_group/newer_group");
  350. QVERIFY(group);
  351. QCOMPARE(group->name(), QString("newer_group"));
  352. // Should fail if the path is invalid.
  353. setInput("a");
  354. execCmd(addGroupCmd, {"mkdir", m_dbFile->fileName(), "/invalid_group/newer_group"});
  355. QVERIFY(m_stderr->readAll().contains("Group /invalid_group not found."));
  356. QCOMPARE(m_stdout->readAll(), QByteArray());
  357. // Should fail to add the root group.
  358. setInput("a");
  359. execCmd(addGroupCmd, {"mkdir", m_dbFile->fileName(), "/"});
  360. QVERIFY(m_stderr->readAll().contains("Group / already exists!"));
  361. QCOMPARE(m_stdout->readAll(), QByteArray());
  362. }
  363. void TestCli::testAnalyze()
  364. {
  365. Analyze analyzeCmd;
  366. QVERIFY(!analyzeCmd.name.isEmpty());
  367. QVERIFY(analyzeCmd.getDescriptionLine().contains(analyzeCmd.name));
  368. const QString hibpPath = QString(KEEPASSX_TEST_DATA_DIR).append("/hibp.txt");
  369. setInput("a");
  370. execCmd(analyzeCmd, {"analyze", "--hibp", hibpPath, m_dbFile->fileName()});
  371. auto output = m_stdout->readAll();
  372. QVERIFY(output.contains("Sample Entry"));
  373. QVERIFY(output.contains("123"));
  374. m_stderr->readLine(); // Skip password prompt
  375. QCOMPARE(m_stderr->readAll(), QByteArray());
  376. }
  377. void TestCli::testClip()
  378. {
  379. QClipboard* clipboard = QGuiApplication::clipboard();
  380. clipboard->clear();
  381. Clip clipCmd;
  382. QVERIFY(!clipCmd.name.isEmpty());
  383. QVERIFY(clipCmd.getDescriptionLine().contains(clipCmd.name));
  384. // Password
  385. setInput("a");
  386. execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry", "0"});
  387. QString errorOutput(m_stderr->readAll());
  388. if (QProcessEnvironment::systemEnvironment().contains("WAYLAND_DISPLAY")) {
  389. QSKIP("Clip test skipped due to QClipboard and Wayland issues");
  390. }
  391. if (errorOutput.contains("Unable to start program")
  392. || errorOutput.contains("No program defined for clipboard manipulation")) {
  393. QSKIP("Clip test skipped due to missing clipboard tool");
  394. }
  395. m_stderr->readLine(); // Skip password prompt
  396. QCOMPARE(m_stderr->readAll(), QByteArray());
  397. QTRY_COMPARE(clipboard->text(), QString("Password"));
  398. QCOMPARE(m_stdout->readLine(), QByteArray("Entry's \"Password\" attribute copied to the clipboard!\n"));
  399. // Quiet option
  400. setInput("a");
  401. execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry", "0", "-q"});
  402. QCOMPARE(m_stderr->readAll(), QByteArray());
  403. QCOMPARE(m_stdout->readAll(), QByteArray());
  404. QTRY_COMPARE(clipboard->text(), QString("Password"));
  405. // Username
  406. setInput("a");
  407. execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry", "0", "-a", "username"});
  408. QTRY_COMPARE(clipboard->text(), QString("User Name"));
  409. // TOTP
  410. setInput("a");
  411. execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry", "0", "--totp"});
  412. QTRY_VERIFY(isTotp(clipboard->text()));
  413. // Test Unicode
  414. setInput("a");
  415. execCmd(clipCmd, {"clip", m_dbFile2->fileName(), "/General/Unicode", "0", "-a", "username"});
  416. QTRY_COMPARE(clipboard->text(), QString(R"(¯\_(ツ)_/¯)"));
  417. // Password with timeout
  418. setInput("a");
  419. // clang-format off
  420. QFuture<void> future = QtConcurrent::run(&clipCmd,
  421. static_cast<int(Clip::*)(const QStringList&)>(&DatabaseCommand::execute),
  422. QStringList{"clip", m_dbFile->fileName(), "/Sample Entry", "1"});
  423. // clang-format on
  424. QTRY_COMPARE(clipboard->text(), QString("Password"));
  425. QTRY_COMPARE_WITH_TIMEOUT(clipboard->text(), QString(""), 2000);
  426. future.waitForFinished();
  427. // TOTP with timeout
  428. setInput("a");
  429. future = QtConcurrent::run(&clipCmd,
  430. static_cast<int (Clip::*)(const QStringList&)>(&DatabaseCommand::execute),
  431. QStringList{"clip", m_dbFile->fileName(), "/Sample Entry", "1", "-t"});
  432. QTRY_VERIFY(isTotp(clipboard->text()));
  433. QTRY_COMPARE_WITH_TIMEOUT(clipboard->text(), QString(""), 2000);
  434. future.waitForFinished();
  435. setInput("a");
  436. execCmd(clipCmd, {"clip", m_dbFile->fileName(), "--totp", "/Sample Entry", "bleuh"});
  437. QVERIFY(m_stderr->readAll().contains("Invalid timeout value bleuh.\n"));
  438. setInput("a");
  439. execCmd(clipCmd, {"clip", m_dbFile2->fileName(), "--totp", "/Sample Entry", "0"});
  440. QVERIFY(m_stderr->readAll().contains("Entry with path /Sample Entry has no TOTP set up.\n"));
  441. setInput("a");
  442. execCmd(clipCmd, {"clip", m_dbFile->fileName(), "-a", "TESTAttribute1", "/Sample Entry", "0"});
  443. QVERIFY(m_stderr->readAll().contains("ERROR: attribute TESTAttribute1 is ambiguous"));
  444. setInput("a");
  445. execCmd(clipCmd, {"clip", m_dbFile2->fileName(), "--attribute", "Username", "--totp", "/Sample Entry", "0"});
  446. QVERIFY(m_stderr->readAll().contains("ERROR: Please specify one of --attribute or --totp, not both.\n"));
  447. // Best option
  448. setInput("a");
  449. execCmd(clipCmd, {"clip", m_dbFileMulti->fileName(), "Multi", "0", "-b"});
  450. QByteArray errorChoices = m_stderr->readAll();
  451. QVERIFY(errorChoices.contains("Multi Entry 1"));
  452. QVERIFY(errorChoices.contains("Multi Entry 2"));
  453. setInput("a");
  454. execCmd(clipCmd, {"clip", m_dbFileMulti->fileName(), "Entry 2", "0", "-b"});
  455. QTRY_COMPARE(clipboard->text(), QString("Password2"));
  456. }
  457. void TestCli::testCreate()
  458. {
  459. Create createCmd;
  460. QVERIFY(!createCmd.name.isEmpty());
  461. QVERIFY(createCmd.getDescriptionLine().contains(createCmd.name));
  462. QScopedPointer<QTemporaryDir> testDir(new QTemporaryDir());
  463. QString dbFilename;
  464. // Testing password option, password mismatch
  465. dbFilename = testDir->path() + "/testCreate_pw.kdbx";
  466. setInput({"a", "b"});
  467. execCmd(createCmd, {"db-create", dbFilename, "-p"});
  468. QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
  469. QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
  470. QCOMPARE(m_stderr->readLine(), QByteArray("Error: Passwords do not match.\n"));
  471. QCOMPARE(m_stderr->readLine(), QByteArray("Failed to set database password.\n"));
  472. // Testing password option
  473. setInput({"a", "a"});
  474. execCmd(createCmd, {"db-create", dbFilename, "-p"});
  475. QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
  476. QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
  477. QCOMPARE(m_stdout->readLine(), QByteArray("Successfully created new database.\n"));
  478. auto db = readDatabase(dbFilename, "a");
  479. QVERIFY(db);
  480. // Testing with empty password (deny it)
  481. dbFilename = testDir->path() + "/testCreate_blankpw.kdbx";
  482. setInput({"", "n"});
  483. execCmd(createCmd, {"db-create", dbFilename, "-p"});
  484. QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
  485. QVERIFY(m_stderr->readLine().contains("empty password"));
  486. QCOMPARE(m_stderr->readLine(), QByteArray("Failed to set database password.\n"));
  487. // Testing with empty password (accept it)
  488. setInput({"", "y"});
  489. execCmd(createCmd, {"db-create", dbFilename, "-p"});
  490. QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
  491. QVERIFY(m_stderr->readLine().contains("empty password"));
  492. QCOMPARE(m_stdout->readLine(), QByteArray("Successfully created new database.\n"));
  493. db = readDatabase(dbFilename, "");
  494. QVERIFY(db);
  495. // Should refuse to create the database if it already exists.
  496. execCmd(createCmd, {"db-create", dbFilename, "-p"});
  497. // Output should be empty when there is an error.
  498. QCOMPARE(m_stdout->readAll(), QByteArray());
  499. QString errorMessage = QString("File " + dbFilename + " already exists.\n");
  500. QCOMPARE(m_stderr->readAll(), errorMessage.toUtf8());
  501. // Should refuse to create without any key provided.
  502. dbFilename = testDir->path() + "/testCreate_key.kdbx";
  503. execCmd(createCmd, {"db-create", dbFilename});
  504. QCOMPARE(m_stdout->readAll(), QByteArray());
  505. QCOMPARE(m_stderr->readLine(), QByteArray("No key is set. Aborting database creation.\n"));
  506. // Testing with keyfile creation
  507. dbFilename = testDir->path() + "/testCreate_key2.kdbx";
  508. QString keyfilePath = testDir->path() + "/keyfile.txt";
  509. setInput({"a", "a"});
  510. execCmd(createCmd, {"db-create", dbFilename, "-p", "-k", keyfilePath});
  511. QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
  512. QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
  513. QCOMPARE(m_stdout->readLine(), QByteArray("Successfully created new database.\n"));
  514. db = readDatabase(dbFilename, "a", keyfilePath);
  515. QVERIFY(db);
  516. // Testing with existing keyfile
  517. dbFilename = testDir->path() + "/testCreate_key3.kdbx";
  518. setInput({"a", "a"});
  519. execCmd(createCmd, {"db-create", dbFilename, "-p", "-k", keyfilePath});
  520. QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
  521. QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
  522. QCOMPARE(m_stdout->readLine(), QByteArray("Successfully created new database.\n"));
  523. db = readDatabase(dbFilename, "a", keyfilePath);
  524. QVERIFY(db);
  525. // Invalid decryption time (format).
  526. dbFilename = testDir->path() + "/testCreate_time.kdbx";
  527. execCmd(createCmd, {"db-create", dbFilename, "-p", "-t", "NAN"});
  528. QCOMPARE(m_stdout->readAll(), QByteArray());
  529. QCOMPARE(m_stderr->readAll(), QByteArray("Invalid decryption time NAN.\n"));
  530. // Invalid decryption time (range).
  531. execCmd(createCmd, {"db-create", dbFilename, "-p", "-t", "10"});
  532. QCOMPARE(m_stdout->readAll(), QByteArray());
  533. QVERIFY(m_stderr->readAll().contains(QByteArray("Target decryption time must be between")));
  534. int encryptionTime = 500;
  535. // Custom encryption time
  536. setInput({"a", "a"});
  537. int epochBefore = QDateTime::currentMSecsSinceEpoch();
  538. execCmd(createCmd, {"db-create", dbFilename, "-p", "-t", QString::number(encryptionTime)});
  539. // Removing 100ms to make sure we account for changes in computation time.
  540. QVERIFY(QDateTime::currentMSecsSinceEpoch() > (epochBefore + encryptionTime - 100));
  541. QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
  542. QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
  543. QCOMPARE(m_stdout->readLine(), QByteArray("Benchmarking key derivation function for 500ms delay.\n"));
  544. QVERIFY(m_stdout->readLine().contains(QByteArray("rounds for key derivation function.\n")));
  545. db = readDatabase(dbFilename, "a");
  546. QVERIFY(db);
  547. }
  548. void TestCli::testInfo()
  549. {
  550. Info infoCmd;
  551. QVERIFY(!infoCmd.name.isEmpty());
  552. QVERIFY(infoCmd.getDescriptionLine().contains(infoCmd.name));
  553. setInput("a");
  554. execCmd(infoCmd, {"db-info", m_dbFile->fileName()});
  555. m_stderr->readLine(); // Skip password prompt
  556. QCOMPARE(m_stderr->readAll(), QByteArray());
  557. QVERIFY(m_stdout->readLine().contains(QByteArray("UUID: ")));
  558. QCOMPARE(m_stdout->readLine(), QByteArray("Name: \n"));
  559. QCOMPARE(m_stdout->readLine(), QByteArray("Description: \n"));
  560. QCOMPARE(m_stdout->readLine(), QByteArray("Cipher: AES 256-bit\n"));
  561. QCOMPARE(m_stdout->readLine(), QByteArray("KDF: AES (6000 rounds)\n"));
  562. QCOMPARE(m_stdout->readLine(), QByteArray("Recycle bin is enabled.\n"));
  563. // Test with quiet option.
  564. setInput("a");
  565. execCmd(infoCmd, {"db-info", "-q", m_dbFile->fileName()});
  566. QCOMPARE(m_stderr->readAll(), QByteArray());
  567. QVERIFY(m_stdout->readLine().contains(QByteArray("UUID: ")));
  568. QCOMPARE(m_stdout->readLine(), QByteArray("Name: \n"));
  569. QCOMPARE(m_stdout->readLine(), QByteArray("Description: \n"));
  570. QCOMPARE(m_stdout->readLine(), QByteArray("Cipher: AES 256-bit\n"));
  571. QCOMPARE(m_stdout->readLine(), QByteArray("KDF: AES (6000 rounds)\n"));
  572. QCOMPARE(m_stdout->readLine(), QByteArray("Recycle bin is enabled.\n"));
  573. }
  574. void TestCli::testDiceware()
  575. {
  576. Diceware dicewareCmd;
  577. QVERIFY(!dicewareCmd.name.isEmpty());
  578. QVERIFY(dicewareCmd.getDescriptionLine().contains(dicewareCmd.name));
  579. execCmd(dicewareCmd, {"diceware"});
  580. QString passphrase(m_stdout->readLine());
  581. QVERIFY(!passphrase.isEmpty());
  582. execCmd(dicewareCmd, {"diceware", "-W", "2"});
  583. passphrase = m_stdout->readLine();
  584. QCOMPARE(passphrase.split(" ").size(), 2);
  585. execCmd(dicewareCmd, {"diceware", "-W", "10"});
  586. passphrase = m_stdout->readLine();
  587. QCOMPARE(passphrase.split(" ").size(), 10);
  588. // Testing with invalid word count
  589. execCmd(dicewareCmd, {"diceware", "-W", "-10"});
  590. QCOMPARE(m_stderr->readLine(), QByteArray("Invalid word count -10\n"));
  591. // Testing with invalid word count format
  592. execCmd(dicewareCmd, {"diceware", "-W", "bleuh"});
  593. QCOMPARE(m_stderr->readLine(), QByteArray("Invalid word count bleuh\n"));
  594. TemporaryFile wordFile;
  595. wordFile.open();
  596. for (int i = 0; i < 4500; ++i) {
  597. wordFile.write(QString("word" + QString::number(i) + "\n").toLatin1());
  598. }
  599. wordFile.close();
  600. execCmd(dicewareCmd, {"diceware", "-W", "11", "-w", wordFile.fileName()});
  601. passphrase = m_stdout->readLine();
  602. const auto words = passphrase.split(" ");
  603. QCOMPARE(words.size(), 11);
  604. QRegularExpression regex("^word\\d+$");
  605. for (const auto& word : words) {
  606. QVERIFY2(regex.match(word).hasMatch(), qPrintable("Word " + word + " was not on the word list"));
  607. }
  608. TemporaryFile smallWordFile;
  609. smallWordFile.open();
  610. for (int i = 0; i < 50; ++i) {
  611. smallWordFile.write(QString("word" + QString::number(i) + "\n").toLatin1());
  612. }
  613. smallWordFile.close();
  614. execCmd(dicewareCmd, {"diceware", "-W", "11", "-w", smallWordFile.fileName()});
  615. QCOMPARE(m_stderr->readLine(), QByteArray("The word list is too small (< 1000 items)\n"));
  616. }
  617. void TestCli::testEdit()
  618. {
  619. Edit editCmd;
  620. QVERIFY(!editCmd.name.isEmpty());
  621. QVERIFY(editCmd.getDescriptionLine().contains(editCmd.name));
  622. setInput("a");
  623. execCmd(editCmd,
  624. {"edit",
  625. "-u",
  626. "newuser",
  627. "--url",
  628. "https://otherurl.example.com/",
  629. "--notes",
  630. "newnotes",
  631. "-t",
  632. "newtitle",
  633. m_dbFile->fileName(),
  634. "/Sample Entry"});
  635. QCOMPARE(m_stdout->readLine(), QByteArray("Successfully edited entry newtitle.\n"));
  636. auto db = readDatabase();
  637. auto* entry = db->rootGroup()->findEntryByPath("/newtitle");
  638. QVERIFY(entry);
  639. QCOMPARE(entry->username(), QString("newuser"));
  640. QCOMPARE(entry->url(), QString("https://otherurl.example.com/"));
  641. QCOMPARE(entry->password(), QString("Password"));
  642. QCOMPARE(entry->notes(), QString("newnotes"));
  643. // Quiet option
  644. setInput("a");
  645. execCmd(editCmd, {"edit", m_dbFile->fileName(), "-q", "-t", "newertitle", "/newtitle"});
  646. QCOMPARE(m_stderr->readAll(), QByteArray());
  647. QCOMPARE(m_stdout->readAll(), QByteArray());
  648. setInput("a");
  649. execCmd(editCmd, {"edit", "-g", m_dbFile->fileName(), "/newertitle"});
  650. db = readDatabase();
  651. entry = db->rootGroup()->findEntryByPath("/newertitle");
  652. QVERIFY(entry);
  653. QCOMPARE(entry->username(), QString("newuser"));
  654. QCOMPARE(entry->url(), QString("https://otherurl.example.com/"));
  655. QVERIFY(!entry->password().isEmpty());
  656. QVERIFY(entry->password() != QString("Password"));
  657. setInput("a");
  658. execCmd(editCmd, {"edit", "-g", "-L", "34", "-t", "evennewertitle", m_dbFile->fileName(), "/newertitle"});
  659. db = readDatabase();
  660. entry = db->rootGroup()->findEntryByPath("/evennewertitle");
  661. QVERIFY(entry);
  662. QCOMPARE(entry->username(), QString("newuser"));
  663. QCOMPARE(entry->url(), QString("https://otherurl.example.com/"));
  664. QVERIFY(entry->password() != QString("Password"));
  665. QCOMPARE(entry->password().size(), 34);
  666. QRegularExpression defaultPasswordClassesRegex("^[a-zA-Z0-9]+$");
  667. QVERIFY(defaultPasswordClassesRegex.match(entry->password()).hasMatch());
  668. setInput("a");
  669. execCmd(editCmd,
  670. {"edit",
  671. "-g",
  672. "-L",
  673. "20",
  674. "--every-group",
  675. "-s",
  676. "-n",
  677. "--upper",
  678. "-l",
  679. m_dbFile->fileName(),
  680. "/evennewertitle"});
  681. QCOMPARE(m_stdout->readAll(), QByteArray("Successfully edited entry evennewertitle.\n"));
  682. db = readDatabase();
  683. entry = db->rootGroup()->findEntryByPath("/evennewertitle");
  684. QVERIFY(entry);
  685. QCOMPARE(entry->password().size(), 20);
  686. QVERIFY(!defaultPasswordClassesRegex.match(entry->password()).hasMatch());
  687. setInput({"a", "newpassword"});
  688. execCmd(editCmd, {"edit", "-p", m_dbFile->fileName(), "/evennewertitle"});
  689. db = readDatabase();
  690. QVERIFY(db);
  691. entry = db->rootGroup()->findEntryByPath("/evennewertitle");
  692. QVERIFY(entry);
  693. QCOMPARE(entry->password(), QString("newpassword"));
  694. // with line break in notes
  695. setInput("a");
  696. execCmd(editCmd, {"edit", m_dbFile->fileName(), "--notes", "testing\\nline breaks", "/evennewertitle"});
  697. db = readDatabase();
  698. entry = db->rootGroup()->findEntryByPath("/evennewertitle");
  699. QVERIFY(entry);
  700. QCOMPARE(entry->notes(), QString("testing\nline breaks"));
  701. }
  702. void TestCli::testEstimate_data()
  703. {
  704. QTest::addColumn<QString>("input");
  705. QTest::addColumn<QString>("length");
  706. QTest::addColumn<QString>("entropy");
  707. QTest::addColumn<QString>("log10");
  708. QTest::addColumn<QStringList>("searchStrings");
  709. QTest::newRow("Dictionary") << "password"
  710. << "8"
  711. << "1.0"
  712. << "0.3" << QStringList{"Type: Dictionary", "\tpassword"};
  713. QTest::newRow("Spatial") << "zxcv"
  714. << "4"
  715. << "10.3"
  716. << "3.1" << QStringList{"Type: Spatial", "\tzxcv"};
  717. QTest::newRow("Spatial(Rep)") << "sdfgsdfg"
  718. << "8"
  719. << "11.3"
  720. << "3.4" << QStringList{"Type: Spatial(Rep)", "\tsdfgsdfg"};
  721. QTest::newRow("Dictionary / Sequence")
  722. << "password123"
  723. << "11"
  724. << "4.5"
  725. << "1.3" << QStringList{"Type: Dictionary", "Type: Sequence", "\tpassword", "\t123"};
  726. QTest::newRow("Dict+Leet") << "p455w0rd"
  727. << "8"
  728. << "2.5"
  729. << "0.7" << QStringList{"Type: Dict+Leet", "\tp455w0rd"};
  730. QTest::newRow("Dictionary(Rep)") << "hellohello"
  731. << "10"
  732. << "7.3"
  733. << "2.2" << QStringList{"Type: Dictionary(Rep)", "\thellohello"};
  734. QTest::newRow("Sequence(Rep) / Dictionary")
  735. << "456456foobar"
  736. << "12"
  737. << "16.7"
  738. << "5.0" << QStringList{"Type: Sequence(Rep)", "Type: Dictionary", "\t456456", "\tfoobar"};
  739. QTest::newRow("Bruteforce(Rep) / Bruteforce")
  740. << "xzxzy"
  741. << "5"
  742. << "16.1"
  743. << "4.8" << QStringList{"Type: Bruteforce(Rep)", "Type: Bruteforce", "\txzxz", "\ty"};
  744. QTest::newRow("Dictionary / Date(Rep)")
  745. << "pass20182018"
  746. << "12"
  747. << "15.1"
  748. << "4.56" << QStringList{"Type: Dictionary", "Type: Date(Rep)", "\tpass", "\t20182018"};
  749. QTest::newRow("Dictionary / Date / Bruteforce")
  750. << "mypass2018-2"
  751. << "12"
  752. << "32.9"
  753. << "9.9" << QStringList{"Type: Dictionary", "Type: Date", "Type: Bruteforce", "\tmypass", "\t2018", "\t-2"};
  754. QTest::newRow("Strong Password") << "E*!%.Qw{t.X,&bafw)\"Q!ah$%;U/"
  755. << "28"
  756. << "165.7"
  757. << "49.8" << QStringList{"Type: Bruteforce", "\tE*"};
  758. // TODO: detect passphrases and adjust entropy calculation accordingly (issue #2347)
  759. QTest::newRow("Strong Passphrase")
  760. << "squint wooing resupply dangle isolation axis headsman"
  761. << "53"
  762. << "151.2"
  763. << "45.5"
  764. << QStringList{
  765. "Type: Dictionary", "Type: Bruteforce", "Multi-word extra bits 22.0", "\tsquint", "\t ", "\twooing"};
  766. }
  767. void TestCli::testEstimate()
  768. {
  769. QFETCH(QString, input);
  770. QFETCH(QString, length);
  771. QFETCH(QString, entropy);
  772. QFETCH(QString, log10);
  773. QFETCH(QStringList, searchStrings);
  774. Estimate estimateCmd;
  775. QVERIFY(!estimateCmd.name.isEmpty());
  776. QVERIFY(estimateCmd.getDescriptionLine().contains(estimateCmd.name));
  777. setInput(input);
  778. execCmd(estimateCmd, {"estimate", "-a"});
  779. auto result = QString(m_stdout->readAll());
  780. QVERIFY(result.contains("Length " + length));
  781. QVERIFY(result.contains("Entropy " + entropy));
  782. QVERIFY(result.contains("Log10 " + log10));
  783. for (const auto& string : asConst(searchStrings)) {
  784. QVERIFY2(result.contains(string), qPrintable("String " + string + " missing"));
  785. }
  786. }
  787. void TestCli::testExport()
  788. {
  789. Export exportCmd;
  790. QVERIFY(!exportCmd.name.isEmpty());
  791. QVERIFY(exportCmd.getDescriptionLine().contains(exportCmd.name));
  792. setInput("a");
  793. execCmd(exportCmd, {"export", m_dbFile->fileName()});
  794. TemporaryFile xmlOutput;
  795. xmlOutput.open(QIODevice::WriteOnly);
  796. xmlOutput.write(m_stdout->readAll());
  797. xmlOutput.close();
  798. QScopedPointer<Database> db(new Database());
  799. QVERIFY(db->import(xmlOutput.fileName()));
  800. auto* entry = db->rootGroup()->findEntryByPath("/Sample Entry");
  801. QVERIFY(entry);
  802. QCOMPARE(entry->password(), QString("Password"));
  803. // Quiet option
  804. QScopedPointer<Database> dbQuiet(new Database());
  805. setInput("a");
  806. execCmd(exportCmd, {"export", "-f", "xml", "-q", m_dbFile->fileName()});
  807. QCOMPARE(m_stderr->readAll(), QByteArray());
  808. xmlOutput.open(QIODevice::WriteOnly);
  809. xmlOutput.write(m_stdout->readAll());
  810. xmlOutput.close();
  811. QVERIFY(db->import(xmlOutput.fileName()));
  812. // CSV exporting
  813. setInput("a");
  814. execCmd(exportCmd, {"export", "-f", "csv", m_dbFile->fileName()});
  815. QByteArray csvHeader = m_stdout->readLine();
  816. QVERIFY(csvHeader.contains(QByteArray("\"Group\",\"Title\",\"Username\",\"Password\",\"URL\",\"Notes\"")));
  817. QByteArray csvData = m_stdout->readAll();
  818. QVERIFY(csvData.contains(QByteArray(
  819. "\"NewDatabase\",\"Sample Entry\",\"User Name\",\"Password\",\"http://www.somesite.com/\",\"Notes\"")));
  820. // test invalid format
  821. setInput("a");
  822. execCmd(exportCmd, {"export", "-f", "yaml", m_dbFile->fileName()});
  823. m_stderr->readLine(); // Skip password prompt
  824. QCOMPARE(m_stderr->readLine(), QByteArray("Unsupported format yaml\n"));
  825. }
  826. void TestCli::testGenerate_data()
  827. {
  828. QTest::addColumn<QStringList>("parameters");
  829. QTest::addColumn<QString>("pattern");
  830. QTest::newRow("default") << QStringList{"generate"} << "^[^\r\n]+$";
  831. QTest::newRow("length") << QStringList{"generate", "-L", "13"} << "^.{13}$";
  832. QTest::newRow("lowercase") << QStringList{"generate", "-L", "14", "-l"} << "^[a-z]{14}$";
  833. QTest::newRow("uppercase") << QStringList{"generate", "-L", "15", "--upper"} << "^[A-Z]{15}$";
  834. QTest::newRow("numbers") << QStringList{"generate", "-L", "16", "-n"} << "^[0-9]{16}$";
  835. QTest::newRow("special") << QStringList{"generate", "-L", "200", "-s"}
  836. << R"(^[\(\)\[\]\{\}\.\-*|\\,:;"'\/\_!+-<=>?#$%&^`@~]{200}$)";
  837. QTest::newRow("special (exclude)") << QStringList{"generate", "-L", "200", "-s", "-x", "+.?@&"}
  838. << R"(^[\(\)\[\]\{\}\.\-*|\\,:;"'\/\_!-<=>#$%^`~]{200}$)";
  839. QTest::newRow("extended") << QStringList{"generate", "-L", "50", "-e"}
  840. << R"(^[^a-zA-Z0-9\(\)\[\]\{\}\.\-\*\|\\,:;"'\/\_!+-<=>?#$%&^`@~]{50}$)";
  841. QTest::newRow("numbers + lowercase + uppercase")
  842. << QStringList{"generate", "-L", "16", "-n", "--upper", "-l"} << "^[0-9a-zA-Z]{16}$";
  843. QTest::newRow("numbers + lowercase + uppercase (exclude)")
  844. << QStringList{"generate", "-L", "500", "-n", "-U", "-l", "-x", "abcdefg0123@"} << "^[^abcdefg0123@]{500}$";
  845. QTest::newRow("numbers + lowercase + uppercase (exclude similar)")
  846. << QStringList{"generate", "-L", "200", "-n", "-U", "-l", "--exclude-similar"} << "^[^l1IO0]{200}$";
  847. QTest::newRow("uppercase + lowercase (every)")
  848. << QStringList{"generate", "-L", "2", "--upper", "-l", "--every-group"} << "^[a-z][A-Z]|[A-Z][a-z]$";
  849. QTest::newRow("numbers + lowercase (every)")
  850. << QStringList{"generate", "-L", "2", "-n", "-l", "--every-group"} << "^[a-z][0-9]|[0-9][a-z]$";
  851. }
  852. void TestCli::testGenerate()
  853. {
  854. QFETCH(QStringList, parameters);
  855. QFETCH(QString, pattern);
  856. Generate generateCmd;
  857. QVERIFY(!generateCmd.name.isEmpty());
  858. QVERIFY(generateCmd.getDescriptionLine().contains(generateCmd.name));
  859. for (int i = 0; i < 10; ++i) {
  860. execCmd(generateCmd, parameters);
  861. QRegularExpression regex(pattern);
  862. #ifdef Q_OS_UNIX
  863. QString password = QString::fromUtf8(m_stdout->readLine());
  864. #else
  865. QString password = QString::fromLatin1(m_stdout->readLine());
  866. #endif
  867. QVERIFY2(regex.match(password).hasMatch(),
  868. qPrintable("Password " + password + " does not match pattern " + pattern));
  869. QCOMPARE(m_stderr->readAll(), QByteArray());
  870. }
  871. // Testing with invalid password length
  872. execCmd(generateCmd, {"generate", "-L", "-10"});
  873. QCOMPARE(m_stderr->readLine(), QByteArray("Invalid password length -10\n"));
  874. execCmd(generateCmd, {"generate", "-L", "0"});
  875. QCOMPARE(m_stderr->readLine(), QByteArray("Invalid password length 0\n"));
  876. // Testing with invalid word count format
  877. execCmd(generateCmd, {"generate", "-L", "bleuh"});
  878. QCOMPARE(m_stderr->readLine(), QByteArray("Invalid password length bleuh\n"));
  879. }
  880. void TestCli::testImport()
  881. {
  882. Import importCmd;
  883. QVERIFY(!importCmd.name.isEmpty());
  884. QVERIFY(importCmd.getDescriptionLine().contains(importCmd.name));
  885. QScopedPointer<QTemporaryDir> testDir(new QTemporaryDir());
  886. QString databaseFilename = testDir->path() + "/testImport1.kdbx";
  887. setInput({"a", "a"});
  888. execCmd(importCmd, {"import", m_xmlFile->fileName(), databaseFilename, "-p"});
  889. QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
  890. QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
  891. QCOMPARE(m_stdout->readLine(), QByteArray("Successfully imported database.\n"));
  892. auto db = readDatabase(databaseFilename, "a");
  893. QVERIFY(db);
  894. auto* entry = db->rootGroup()->findEntryByPath("/Sample Entry 1");
  895. QVERIFY(entry);
  896. QCOMPARE(entry->username(), QString("User Name"));
  897. // Should refuse to create the database if it already exists.
  898. execCmd(importCmd, {"import", m_xmlFile->fileName(), databaseFilename});
  899. // Output should be empty when there is an error.
  900. QCOMPARE(m_stdout->readAll(), QByteArray());
  901. QString errorMessage = QString("File " + databaseFilename + " already exists.\n");
  902. QCOMPARE(m_stderr->readAll(), errorMessage.toUtf8());
  903. // Testing import with non-existing keyfile
  904. databaseFilename = testDir->path() + "/testImport2.kdbx";
  905. QString keyfilePath = testDir->path() + "/keyfile.txt";
  906. setInput({"a", "a"});
  907. execCmd(importCmd, {"import", "-p", "-k", keyfilePath, m_xmlFile->fileName(), databaseFilename});
  908. QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
  909. QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
  910. QCOMPARE(m_stdout->readLine(), QByteArray("Successfully imported database.\n"));
  911. db = readDatabase(databaseFilename, "a", keyfilePath);
  912. QVERIFY(db);
  913. // Testing import with existing keyfile
  914. databaseFilename = testDir->path() + "/testImport3.kdbx";
  915. setInput({"a", "a"});
  916. execCmd(importCmd, {"import", "-p", "-k", keyfilePath, m_xmlFile->fileName(), databaseFilename});
  917. QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
  918. QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
  919. QCOMPARE(m_stdout->readLine(), QByteArray("Successfully imported database.\n"));
  920. db = readDatabase(databaseFilename, "a", keyfilePath);
  921. QVERIFY(db);
  922. // Invalid decryption time (format).
  923. databaseFilename = testDir->path() + "/testCreate_time.kdbx";
  924. execCmd(importCmd, {"import", "-p", "-t", "NAN", m_xmlFile->fileName(), databaseFilename});
  925. QCOMPARE(m_stdout->readAll(), QByteArray());
  926. QCOMPARE(m_stderr->readAll(), QByteArray("Invalid decryption time NAN.\n"));
  927. // Invalid decryption time (range).
  928. execCmd(importCmd, {"import", "-p", "-t", "10", m_xmlFile->fileName(), databaseFilename});
  929. QCOMPARE(m_stdout->readAll(), QByteArray());
  930. QVERIFY(m_stderr->readAll().contains(QByteArray("Target decryption time must be between")));
  931. int encryptionTime = 500;
  932. // Custom encryption time
  933. setInput({"a", "a"});
  934. int epochBefore = QDateTime::currentMSecsSinceEpoch();
  935. execCmd(importCmd,
  936. {"import", "-p", "-t", QString::number(encryptionTime), m_xmlFile->fileName(), databaseFilename});
  937. // Removing 100ms to make sure we account for changes in computation time.
  938. QVERIFY(QDateTime::currentMSecsSinceEpoch() > (epochBefore + encryptionTime - 100));
  939. QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
  940. QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
  941. QCOMPARE(m_stdout->readLine(), QByteArray("Benchmarking key derivation function for 500ms delay.\n"));
  942. QVERIFY(m_stdout->readLine().contains(QByteArray("rounds for key derivation function.\n")));
  943. db = readDatabase(databaseFilename, "a");
  944. QVERIFY(db);
  945. // Quiet option
  946. QScopedPointer<QTemporaryDir> testDirQuiet(new QTemporaryDir());
  947. QString databaseFilenameQuiet = testDirQuiet->path() + "/testImport2.kdbx";
  948. setInput({"a", "a"});
  949. execCmd(importCmd, {"import", "-p", "-q", m_xmlFile->fileName(), databaseFilenameQuiet});
  950. QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
  951. QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
  952. QCOMPARE(m_stdout->readLine(), QByteArray());
  953. db = readDatabase(databaseFilenameQuiet, "a");
  954. QVERIFY(db);
  955. }
  956. void TestCli::testKeyFileOption()
  957. {
  958. List listCmd;
  959. QString keyFilePath(QString(KEEPASSX_TEST_DATA_DIR).append("/KeyFileProtected.key"));
  960. setInput("a");
  961. execCmd(listCmd, {"ls", "-k", keyFilePath, m_keyFileProtectedDbFile->fileName()});
  962. m_stderr->readLine(); // Skip password prompt
  963. QCOMPARE(m_stderr->readAll(), QByteArray());
  964. QCOMPARE(m_stdout->readAll(),
  965. QByteArray("entry1\n"
  966. "entry2\n"));
  967. // Should raise an error with no key file.
  968. setInput("a");
  969. execCmd(listCmd, {"ls", m_keyFileProtectedDbFile->fileName()});
  970. QCOMPARE(m_stdout->readAll(), QByteArray());
  971. QVERIFY(m_stderr->readAll().contains("Invalid credentials were provided"));
  972. // Should raise an error if key file path is invalid.
  973. setInput("a");
  974. execCmd(listCmd, {"ls", "-k", "invalidpath", m_keyFileProtectedDbFile->fileName()});
  975. m_stderr->readLine(); // skip password prompt
  976. QCOMPARE(m_stdout->readAll(), QByteArray());
  977. QCOMPARE(m_stderr->readAll().split(':').at(0), QByteArray("Failed to load key file invalidpath"));
  978. }
  979. void TestCli::testNoPasswordOption()
  980. {
  981. List listCmd;
  982. QString keyFilePath(QString(KEEPASSX_TEST_DATA_DIR).append("/KeyFileProtectedNoPassword.key"));
  983. execCmd(listCmd, {"ls", "-k", keyFilePath, "--no-password", m_keyFileProtectedNoPasswordDbFile->fileName()});
  984. // Expecting no password prompt
  985. QCOMPARE(m_stderr->readAll(), QByteArray());
  986. QCOMPARE(m_stdout->readAll(),
  987. QByteArray("entry1\n"
  988. "entry2\n"));
  989. // Should raise an error with no key file.
  990. execCmd(listCmd, {"ls", "--no-password", m_keyFileProtectedNoPasswordDbFile->fileName()});
  991. QCOMPARE(m_stdout->readAll(), QByteArray());
  992. QVERIFY(m_stderr->readAll().contains("Invalid credentials were provided"));
  993. }
  994. void TestCli::testList()
  995. {
  996. List listCmd;
  997. QVERIFY(!listCmd.name.isEmpty());
  998. QVERIFY(listCmd.getDescriptionLine().contains(listCmd.name));
  999. setInput("a");
  1000. execCmd(listCmd, {"ls", m_dbFile->fileName()});
  1001. m_stderr->readLine(); // Skip password prompt
  1002. QCOMPARE(m_stderr->readAll(), QByteArray());
  1003. QCOMPARE(m_stdout->readAll(),
  1004. QByteArray("Sample Entry\n"
  1005. "General/\n"
  1006. "Windows/\n"
  1007. "Network/\n"
  1008. "Internet/\n"
  1009. "eMail/\n"
  1010. "Homebanking/\n"));
  1011. // Quiet option
  1012. setInput("a");
  1013. execCmd(listCmd, {"ls", "-q", m_dbFile->fileName()});
  1014. QCOMPARE(m_stderr->readAll(), QByteArray());
  1015. QCOMPARE(m_stdout->readAll(),
  1016. QByteArray("Sample Entry\n"
  1017. "General/\n"
  1018. "Windows/\n"
  1019. "Network/\n"
  1020. "Internet/\n"
  1021. "eMail/\n"
  1022. "Homebanking/\n"));
  1023. setInput("a");
  1024. execCmd(listCmd, {"ls", "-R", m_dbFile->fileName()});
  1025. QCOMPARE(m_stdout->readAll(),
  1026. QByteArray("Sample Entry\n"
  1027. "General/\n"
  1028. " [empty]\n"
  1029. "Windows/\n"
  1030. " [empty]\n"
  1031. "Network/\n"
  1032. " [empty]\n"
  1033. "Internet/\n"
  1034. " [empty]\n"
  1035. "eMail/\n"
  1036. " [empty]\n"
  1037. "Homebanking/\n"
  1038. " Subgroup/\n"
  1039. " Subgroup Entry\n"));
  1040. setInput("a");
  1041. execCmd(listCmd, {"ls", "-R", "-f", m_dbFile->fileName()});
  1042. QCOMPARE(m_stdout->readAll(),
  1043. QByteArray("Sample Entry\n"
  1044. "General/\n"
  1045. "General/[empty]\n"
  1046. "Windows/\n"
  1047. "Windows/[empty]\n"
  1048. "Network/\n"
  1049. "Network/[empty]\n"
  1050. "Internet/\n"
  1051. "Internet/[empty]\n"
  1052. "eMail/\n"
  1053. "eMail/[empty]\n"
  1054. "Homebanking/\n"
  1055. "Homebanking/Subgroup/\n"
  1056. "Homebanking/Subgroup/Subgroup Entry\n"));
  1057. setInput("a");
  1058. execCmd(listCmd, {"ls", "-R", "-f", m_dbFile->fileName(), "/Homebanking"});
  1059. QCOMPARE(m_stdout->readAll(),
  1060. QByteArray("Subgroup/\n"
  1061. "Subgroup/Subgroup Entry\n"));
  1062. setInput("a");
  1063. execCmd(listCmd, {"ls", m_dbFile->fileName(), "/General/"});
  1064. QCOMPARE(m_stdout->readAll(), QByteArray("[empty]\n"));
  1065. setInput("a");
  1066. execCmd(listCmd, {"ls", m_dbFile->fileName(), "/DoesNotExist/"});
  1067. m_stderr->readLine(); // skip password prompt
  1068. QCOMPARE(m_stderr->readAll(), QByteArray("Cannot find group /DoesNotExist/.\n"));
  1069. QCOMPARE(m_stdout->readAll(), QByteArray());
  1070. }
  1071. void TestCli::testLocate()
  1072. {
  1073. Locate locateCmd;
  1074. QVERIFY(!locateCmd.name.isEmpty());
  1075. QVERIFY(locateCmd.getDescriptionLine().contains(locateCmd.name));
  1076. setInput("a");
  1077. execCmd(locateCmd, {"locate", m_dbFile->fileName(), "Sample"});
  1078. m_stderr->readLine(); // Skip password prompt
  1079. QCOMPARE(m_stderr->readAll(), QByteArray());
  1080. QCOMPARE(m_stdout->readAll(), QByteArray("/Sample Entry\n"));
  1081. // Quiet option
  1082. setInput("a");
  1083. execCmd(locateCmd, {"locate", m_dbFile->fileName(), "-q", "Sample"});
  1084. QCOMPARE(m_stderr->readAll(), QByteArray());
  1085. QCOMPARE(m_stdout->readAll(), QByteArray("/Sample Entry\n"));
  1086. setInput("a");
  1087. execCmd(locateCmd, {"locate", m_dbFile->fileName(), "Does Not Exist"});
  1088. m_stderr->readLine(); // skip password prompt
  1089. QCOMPARE(m_stderr->readAll(), QByteArray("No results for that search term.\n"));
  1090. QCOMPARE(m_stdout->readAll(), QByteArray());
  1091. // write a modified database
  1092. auto db = readDatabase();
  1093. QVERIFY(db);
  1094. auto* group = db->rootGroup()->findGroupByPath("/General/");
  1095. QVERIFY(group);
  1096. auto* entry = new Entry();
  1097. entry->setUuid(QUuid::createUuid());
  1098. entry->setTitle("New Entry");
  1099. group->addEntry(entry);
  1100. TemporaryFile tmpFile;
  1101. tmpFile.open();
  1102. tmpFile.close();
  1103. db->saveAs(tmpFile.fileName());
  1104. setInput("a");
  1105. execCmd(locateCmd, {"locate", tmpFile.fileName(), "New"});
  1106. QCOMPARE(m_stdout->readAll(), QByteArray("/General/New Entry\n"));
  1107. setInput("a");
  1108. execCmd(locateCmd, {"locate", tmpFile.fileName(), "Entry"});
  1109. QCOMPARE(m_stdout->readAll(),
  1110. QByteArray("/Sample Entry\n/General/New Entry\n/Homebanking/Subgroup/Subgroup Entry\n"));
  1111. }
  1112. void TestCli::testMerge()
  1113. {
  1114. Merge mergeCmd;
  1115. QVERIFY(!mergeCmd.name.isEmpty());
  1116. QVERIFY(mergeCmd.getDescriptionLine().contains(mergeCmd.name));
  1117. // load test database and save copies
  1118. auto db = readDatabase();
  1119. QVERIFY(db);
  1120. TemporaryFile targetFile1;
  1121. targetFile1.open();
  1122. targetFile1.close();
  1123. TemporaryFile targetFile2;
  1124. targetFile2.open();
  1125. targetFile2.close();
  1126. TemporaryFile targetFile3;
  1127. targetFile3.open();
  1128. targetFile3.close();
  1129. db->saveAs(targetFile1.fileName());
  1130. db->saveAs(targetFile2.fileName());
  1131. // save another copy with a different password
  1132. auto oldKey = db->key();
  1133. auto key = QSharedPointer<CompositeKey>::create();
  1134. key->addKey(QSharedPointer<PasswordKey>::create("b"));
  1135. db->setKey(key);
  1136. db->saveAs(targetFile3.fileName());
  1137. // Restore the original password
  1138. db->setKey(oldKey);
  1139. // then add a new entry to the in-memory database and save another copy
  1140. auto* entry = new Entry();
  1141. entry->setUuid(QUuid::createUuid());
  1142. entry->setTitle("Some Website");
  1143. entry->setPassword("secretsecretsecret");
  1144. auto* group = db->rootGroup()->findGroupByPath("/Internet/");
  1145. QVERIFY(group);
  1146. group->addEntry(entry);
  1147. TemporaryFile sourceFile;
  1148. sourceFile.open();
  1149. sourceFile.close();
  1150. db->saveAs(sourceFile.fileName());
  1151. setInput("a");
  1152. execCmd(mergeCmd, {"merge", "-s", targetFile1.fileName(), sourceFile.fileName()});
  1153. m_stderr->readLine(); // Skip password prompt
  1154. QCOMPARE(m_stderr->readAll(), QByteArray());
  1155. QList<QByteArray> outLines1 = m_stdout->readAll().split('\n');
  1156. QVERIFY(outLines1.at(0).contains("Overwriting Internet"));
  1157. QVERIFY(outLines1.at(1).contains("Creating missing Some Website"));
  1158. QCOMPARE(outLines1.at(2),
  1159. QString("Successfully merged %1 into %2.").arg(sourceFile.fileName(), targetFile1.fileName()).toUtf8());
  1160. auto mergedDb = QSharedPointer<Database>::create();
  1161. QVERIFY(mergedDb->open(targetFile1.fileName(), oldKey));
  1162. auto* entry1 = mergedDb->rootGroup()->findEntryByPath("/Internet/Some Website");
  1163. QVERIFY(entry1);
  1164. QCOMPARE(entry1->title(), QString("Some Website"));
  1165. QCOMPARE(entry1->password(), QString("secretsecretsecret"));
  1166. // the dry run option should not modify the target database.
  1167. setInput("a");
  1168. execCmd(mergeCmd, {"merge", "--dry-run", "-s", targetFile2.fileName(), sourceFile.fileName()});
  1169. QList<QByteArray> outLines2 = m_stdout->readAll().split('\n');
  1170. QVERIFY(outLines2.at(0).contains("Overwriting Internet"));
  1171. QVERIFY(outLines2.at(1).contains("Creating missing Some Website"));
  1172. QCOMPARE(outLines2.at(2), QByteArray("Database was not modified by merge operation."));
  1173. mergedDb = QSharedPointer<Database>::create();
  1174. QVERIFY(mergedDb->open(targetFile2.fileName(), oldKey));
  1175. entry1 = mergedDb->rootGroup()->findEntryByPath("/Internet/Some Website");
  1176. QVERIFY(!entry1);
  1177. // the dry run option can be used with the quiet option
  1178. setInput("a");
  1179. execCmd(mergeCmd, {"merge", "--dry-run", "-s", "-q", targetFile2.fileName(), sourceFile.fileName()});
  1180. QCOMPARE(m_stderr->readAll(), QByteArray());
  1181. QCOMPARE(m_stdout->readAll(), QByteArray());
  1182. mergedDb = QSharedPointer<Database>::create();
  1183. QVERIFY(mergedDb->open(targetFile2.fileName(), oldKey));
  1184. entry1 = mergedDb->rootGroup()->findEntryByPath("/Internet/Some Website");
  1185. QVERIFY(!entry1);
  1186. // try again with different passwords for both files
  1187. setInput({"b", "a"});
  1188. execCmd(mergeCmd, {"merge", targetFile3.fileName(), sourceFile.fileName()});
  1189. QList<QByteArray> outLines3 = m_stdout->readAll().split('\n');
  1190. QCOMPARE(outLines3.at(2),
  1191. QString("Successfully merged %1 into %2.").arg(sourceFile.fileName(), targetFile3.fileName()).toUtf8());
  1192. mergedDb = QSharedPointer<Database>::create();
  1193. QVERIFY(mergedDb->open(targetFile3.fileName(), key));
  1194. entry1 = mergedDb->rootGroup()->findEntryByPath("/Internet/Some Website");
  1195. QVERIFY(entry1);
  1196. QCOMPARE(entry1->title(), QString("Some Website"));
  1197. QCOMPARE(entry1->password(), QString("secretsecretsecret"));
  1198. // making sure that the message is different if the database was not
  1199. // modified by the merge operation.
  1200. setInput("a");
  1201. execCmd(mergeCmd, {"merge", "-s", sourceFile.fileName(), sourceFile.fileName()});
  1202. QCOMPARE(m_stdout->readAll(), QByteArray("Database was not modified by merge operation.\n"));
  1203. // Quiet option
  1204. setInput("a");
  1205. execCmd(mergeCmd, {"merge", "-q", "-s", sourceFile.fileName(), sourceFile.fileName()});
  1206. QCOMPARE(m_stderr->readAll(), QByteArray());
  1207. QCOMPARE(m_stdout->readAll(), QByteArray());
  1208. // Quiet option without the -s option
  1209. setInput({"a", "a"});
  1210. execCmd(mergeCmd, {"merge", "-q", sourceFile.fileName(), sourceFile.fileName()});
  1211. QCOMPARE(m_stderr->readAll(), QByteArray());
  1212. QCOMPARE(m_stdout->readAll(), QByteArray());
  1213. }
  1214. void TestCli::testMergeWithKeys()
  1215. {
  1216. Create createCmd;
  1217. QVERIFY(!createCmd.name.isEmpty());
  1218. QVERIFY(createCmd.getDescriptionLine().contains(createCmd.name));
  1219. Merge mergeCmd;
  1220. QVERIFY(!mergeCmd.name.isEmpty());
  1221. QVERIFY(mergeCmd.getDescriptionLine().contains(mergeCmd.name));
  1222. QScopedPointer<QTemporaryDir> testDir(new QTemporaryDir());
  1223. QString sourceDatabaseFilename = testDir->path() + "/testSourceDatabase.kdbx";
  1224. QString sourceKeyfilePath = testDir->path() + "/testSourceKeyfile.txt";
  1225. QString targetDatabaseFilename = testDir->path() + "/testTargetDatabase.kdbx";
  1226. QString targetKeyfilePath = testDir->path() + "/testTargetKeyfile.txt";
  1227. setInput({"a", "a"});
  1228. execCmd(createCmd, {"db-create", sourceDatabaseFilename, "-p", "-k", sourceKeyfilePath});
  1229. setInput({"b", "b"});
  1230. execCmd(createCmd, {"db-create", targetDatabaseFilename, "-p", "-k", targetKeyfilePath});
  1231. auto sourceDatabase = readDatabase(sourceDatabaseFilename, "a", sourceKeyfilePath);
  1232. QVERIFY(sourceDatabase);
  1233. auto targetDatabase = readDatabase(targetDatabaseFilename, "b", targetKeyfilePath);
  1234. QVERIFY(targetDatabase);
  1235. auto* rootGroup = new Group();
  1236. rootGroup->setName("root");
  1237. rootGroup->setUuid(QUuid::createUuid());
  1238. auto* group = new Group();
  1239. group->setUuid(QUuid::createUuid());
  1240. group->setParent(rootGroup);
  1241. group->setName("Internet");
  1242. auto* entry = new Entry();
  1243. entry->setUuid(QUuid::createUuid());
  1244. entry->setTitle("Some Website");
  1245. entry->setPassword("secretsecretsecret");
  1246. group->addEntry(entry);
  1247. sourceDatabase->setRootGroup(rootGroup);
  1248. auto* otherRootGroup = new Group();
  1249. otherRootGroup->setName("root");
  1250. otherRootGroup->setUuid(QUuid::createUuid());
  1251. auto* otherGroup = new Group();
  1252. otherGroup->setUuid(QUuid::createUuid());
  1253. otherGroup->setParent(otherRootGroup);
  1254. otherGroup->setName("Internet");
  1255. auto* otherEntry = new Entry();
  1256. otherEntry->setUuid(QUuid::createUuid());
  1257. otherEntry->setTitle("Some Website 2");
  1258. otherEntry->setPassword("secretsecretsecret 2");
  1259. otherGroup->addEntry(otherEntry);
  1260. targetDatabase->setRootGroup(otherRootGroup);
  1261. sourceDatabase->saveAs(sourceDatabaseFilename);
  1262. targetDatabase->saveAs(targetDatabaseFilename);
  1263. setInput({"b", "a"});
  1264. execCmd(mergeCmd,
  1265. {"merge",
  1266. "-k",
  1267. targetKeyfilePath,
  1268. "--key-file-from",
  1269. sourceKeyfilePath,
  1270. targetDatabaseFilename,
  1271. sourceDatabaseFilename});
  1272. QList<QByteArray> lines = m_stdout->readAll().split('\n');
  1273. QVERIFY(lines.contains(
  1274. QString("Successfully merged %1 into %2.").arg(sourceDatabaseFilename, targetDatabaseFilename).toUtf8()));
  1275. }
  1276. void TestCli::testMove()
  1277. {
  1278. Move moveCmd;
  1279. QVERIFY(!moveCmd.name.isEmpty());
  1280. QVERIFY(moveCmd.getDescriptionLine().contains(moveCmd.name));
  1281. setInput("a");
  1282. execCmd(moveCmd, {"mv", m_dbFile->fileName(), "invalid_entry_path", "invalid_group_path"});
  1283. m_stderr->readLine(); // skip password prompt
  1284. QCOMPARE(m_stderr->readLine(), QByteArray("Could not find entry with path invalid_entry_path.\n"));
  1285. QCOMPARE(m_stdout->readLine(), QByteArray());
  1286. setInput("a");
  1287. execCmd(moveCmd, {"mv", m_dbFile->fileName(), "Sample Entry", "invalid_group_path"});
  1288. m_stderr->readLine(); // skip password prompt
  1289. QCOMPARE(m_stderr->readLine(), QByteArray("Could not find group with path invalid_group_path.\n"));
  1290. QCOMPARE(m_stdout->readLine(), QByteArray());
  1291. setInput("a");
  1292. execCmd(moveCmd, {"mv", m_dbFile->fileName(), "Sample Entry", "General/"});
  1293. m_stderr->readLine(); // skip password prompt
  1294. QCOMPARE(m_stderr->readLine(), QByteArray());
  1295. QCOMPARE(m_stdout->readLine(), QByteArray("Successfully moved entry Sample Entry to group General/.\n"));
  1296. auto db = readDatabase();
  1297. auto* entry = db->rootGroup()->findEntryByPath("General/Sample Entry");
  1298. QVERIFY(entry);
  1299. // Test that not modified if the same group is destination.
  1300. setInput("a");
  1301. execCmd(moveCmd, {"mv", m_dbFile->fileName(), "General/Sample Entry", "General/"});
  1302. m_stderr->readLine(); // skip password prompt
  1303. QCOMPARE(m_stderr->readLine(), QByteArray("Entry is already in group General/.\n"));
  1304. QCOMPARE(m_stdout->readLine(), QByteArray());
  1305. // sanity check
  1306. db = readDatabase();
  1307. entry = db->rootGroup()->findEntryByPath("General/Sample Entry");
  1308. QVERIFY(entry);
  1309. }
  1310. void TestCli::testRemove()
  1311. {
  1312. Remove removeCmd;
  1313. QVERIFY(!removeCmd.name.isEmpty());
  1314. QVERIFY(removeCmd.getDescriptionLine().contains(removeCmd.name));
  1315. // load test database and save a copy with disabled recycle bin
  1316. auto db = readDatabase();
  1317. QVERIFY(db);
  1318. TemporaryFile fileCopy;
  1319. fileCopy.open();
  1320. fileCopy.close();
  1321. db->metadata()->setRecycleBinEnabled(false);
  1322. db->saveAs(fileCopy.fileName());
  1323. // delete entry and verify
  1324. setInput("a");
  1325. execCmd(removeCmd, {"rm", m_dbFile->fileName(), "/Sample Entry"});
  1326. m_stderr->readLine(); // skip password prompt
  1327. QCOMPARE(m_stderr->readAll(), QByteArray());
  1328. QCOMPARE(m_stdout->readAll(), QByteArray("Successfully recycled entry Sample Entry.\n"));
  1329. auto readBackDb = readDatabase();
  1330. QVERIFY(readBackDb);
  1331. QVERIFY(!readBackDb->rootGroup()->findEntryByPath("/Sample Entry"));
  1332. QVERIFY(readBackDb->rootGroup()->findEntryByPath(QString("/%1/Sample Entry").arg(Group::tr("Recycle Bin"))));
  1333. // try again, this time without recycle bin
  1334. setInput("a");
  1335. execCmd(removeCmd, {"rm", fileCopy.fileName(), "/Sample Entry"});
  1336. m_stderr->readLine(); // skip password prompt
  1337. QCOMPARE(m_stdout->readAll(), QByteArray("Successfully deleted entry Sample Entry.\n"));
  1338. readBackDb = readDatabase(fileCopy.fileName(), "a");
  1339. QVERIFY(readBackDb);
  1340. QVERIFY(!readBackDb->rootGroup()->findEntryByPath("/Sample Entry"));
  1341. QVERIFY(!readBackDb->rootGroup()->findEntryByPath(QString("/%1/Sample Entry").arg(Group::tr("Recycle Bin"))));
  1342. // finally, try deleting a non-existent entry
  1343. setInput("a");
  1344. execCmd(removeCmd, {"rm", fileCopy.fileName(), "/Sample Entry"});
  1345. m_stderr->readLine(); // skip password prompt
  1346. QCOMPARE(m_stderr->readAll(), QByteArray("Entry /Sample Entry not found.\n"));
  1347. QCOMPARE(m_stdout->readAll(), QByteArray());
  1348. // try deleting a directory, should fail
  1349. setInput("a");
  1350. execCmd(removeCmd, {"rm", fileCopy.fileName(), "/General"});
  1351. m_stderr->readLine(); // skip password prompt
  1352. QCOMPARE(m_stderr->readAll(), QByteArray("Entry /General not found.\n"));
  1353. QCOMPARE(m_stdout->readAll(), QByteArray());
  1354. }
  1355. void TestCli::testRemoveGroup()
  1356. {
  1357. RemoveGroup removeGroupCmd;
  1358. QVERIFY(!removeGroupCmd.name.isEmpty());
  1359. QVERIFY(removeGroupCmd.getDescriptionLine().contains(removeGroupCmd.name));
  1360. // try deleting a directory, should recycle it first.
  1361. setInput("a");
  1362. execCmd(removeGroupCmd, {"rmdir", m_dbFile->fileName(), "/General"});
  1363. m_stderr->readLine(); // skip password prompt
  1364. QCOMPARE(m_stderr->readAll(), QByteArray());
  1365. QCOMPARE(m_stdout->readAll(), QByteArray("Successfully recycled group /General.\n"));
  1366. auto db = readDatabase();
  1367. auto* group = db->rootGroup()->findGroupByPath("General");
  1368. QVERIFY(!group);
  1369. // try deleting a directory again, should delete it permanently.
  1370. setInput("a");
  1371. execCmd(removeGroupCmd, {"rmdir", m_dbFile->fileName(), "Recycle Bin/General"});
  1372. m_stderr->readLine(); // skip password prompt
  1373. QCOMPARE(m_stderr->readAll(), QByteArray());
  1374. QCOMPARE(m_stdout->readAll(), QByteArray("Successfully deleted group Recycle Bin/General.\n"));
  1375. db = readDatabase();
  1376. group = db->rootGroup()->findGroupByPath("Recycle Bin/General");
  1377. QVERIFY(!group);
  1378. // try deleting an invalid group, should fail.
  1379. setInput("a");
  1380. execCmd(removeGroupCmd, {"rmdir", m_dbFile->fileName(), "invalid"});
  1381. m_stderr->readLine(); // skip password prompt
  1382. QCOMPARE(m_stderr->readAll(), QByteArray("Group invalid not found.\n"));
  1383. QCOMPARE(m_stdout->readAll(), QByteArray());
  1384. // Should fail to remove the root group.
  1385. setInput("a");
  1386. execCmd(removeGroupCmd, {"rmdir", m_dbFile->fileName(), "/"});
  1387. m_stderr->readLine(); // skip password prompt
  1388. QCOMPARE(m_stderr->readAll(), QByteArray("Cannot remove root group from database.\n"));
  1389. QCOMPARE(m_stdout->readAll(), QByteArray());
  1390. }
  1391. void TestCli::testRemoveQuiet()
  1392. {
  1393. Remove removeCmd;
  1394. QVERIFY(!removeCmd.name.isEmpty());
  1395. QVERIFY(removeCmd.getDescriptionLine().contains(removeCmd.name));
  1396. // delete entry and verify
  1397. setInput("a");
  1398. execCmd(removeCmd, {"rm", "-q", m_dbFile->fileName(), "/Sample Entry"});
  1399. QCOMPARE(m_stderr->readAll(), QByteArray());
  1400. QCOMPARE(m_stdout->readAll(), QByteArray());
  1401. auto db = readDatabase();
  1402. QVERIFY(db);
  1403. QVERIFY(!db->rootGroup()->findEntryByPath("/Sample Entry"));
  1404. QVERIFY(db->rootGroup()->findEntryByPath(QString("/%1/Sample Entry").arg(Group::tr("Recycle Bin"))));
  1405. // remove the entry completely
  1406. setInput("a");
  1407. execCmd(removeCmd, {"rm", "-q", m_dbFile->fileName(), QString("/%1/Sample Entry").arg(Group::tr("Recycle Bin"))});
  1408. QCOMPARE(m_stderr->readAll(), QByteArray());
  1409. QCOMPARE(m_stdout->readAll(), QByteArray());
  1410. db = readDatabase();
  1411. QVERIFY(!db->rootGroup()->findEntryByPath("/Sample Entry"));
  1412. QVERIFY(!db->rootGroup()->findEntryByPath(QString("/%1/Sample Entry").arg(Group::tr("Recycle Bin"))));
  1413. }
  1414. void TestCli::testShow()
  1415. {
  1416. Show showCmd;
  1417. QVERIFY(!showCmd.name.isEmpty());
  1418. QVERIFY(showCmd.getDescriptionLine().contains(showCmd.name));
  1419. setInput("a");
  1420. execCmd(showCmd, {"show", m_dbFile->fileName(), "/Sample Entry"});
  1421. m_stderr->readLine(); // Skip password prompt
  1422. QCOMPARE(m_stderr->readAll(), QByteArray());
  1423. QCOMPARE(m_stdout->readAll(),
  1424. QByteArray("Title: Sample Entry\n"
  1425. "UserName: User Name\n"
  1426. "Password: PROTECTED\n"
  1427. "URL: http://www.somesite.com/\n"
  1428. "Notes: Notes\n"));
  1429. setInput("a");
  1430. execCmd(showCmd, {"show", "-s", m_dbFile->fileName(), "/Sample Entry"});
  1431. QCOMPARE(m_stdout->readAll(),
  1432. QByteArray("Title: Sample Entry\n"
  1433. "UserName: User Name\n"
  1434. "Password: Password\n"
  1435. "URL: http://www.somesite.com/\n"
  1436. "Notes: Notes\n"));
  1437. setInput("a");
  1438. execCmd(showCmd, {"show", m_dbFile->fileName(), "-q", "/Sample Entry"});
  1439. QCOMPARE(m_stderr->readAll(), QByteArray());
  1440. QCOMPARE(m_stdout->readAll(),
  1441. QByteArray("Title: Sample Entry\n"
  1442. "UserName: User Name\n"
  1443. "Password: PROTECTED\n"
  1444. "URL: http://www.somesite.com/\n"
  1445. "Notes: Notes\n"));
  1446. setInput("a");
  1447. execCmd(showCmd, {"show", "-a", "Title", m_dbFile->fileName(), "/Sample Entry"});
  1448. QCOMPARE(m_stdout->readAll(), QByteArray("Sample Entry\n"));
  1449. setInput("a");
  1450. execCmd(showCmd, {"show", "-a", "Password", m_dbFile->fileName(), "/Sample Entry"});
  1451. QCOMPARE(m_stdout->readAll(), QByteArray("Password\n"));
  1452. setInput("a");
  1453. execCmd(showCmd, {"show", "-a", "Title", "-a", "URL", m_dbFile->fileName(), "/Sample Entry"});
  1454. QCOMPARE(m_stdout->readAll(),
  1455. QByteArray("Sample Entry\n"
  1456. "http://www.somesite.com/\n"));
  1457. // Test case insensitivity
  1458. setInput("a");
  1459. execCmd(showCmd, {"show", "-a", "TITLE", "-a", "URL", m_dbFile->fileName(), "/Sample Entry"});
  1460. QCOMPARE(m_stdout->readAll(),
  1461. QByteArray("Sample Entry\n"
  1462. "http://www.somesite.com/\n"));
  1463. setInput("a");
  1464. execCmd(showCmd, {"show", "-a", "DoesNotExist", m_dbFile->fileName(), "/Sample Entry"});
  1465. QCOMPARE(m_stdout->readAll(), QByteArray());
  1466. QVERIFY(m_stderr->readAll().contains("ERROR: unknown attribute DoesNotExist.\n"));
  1467. setInput("a");
  1468. execCmd(showCmd, {"show", "-t", m_dbFile->fileName(), "/Sample Entry"});
  1469. QVERIFY(isTotp(m_stdout->readAll()));
  1470. setInput("a");
  1471. execCmd(showCmd, {"show", "-a", "Title", m_dbFile->fileName(), "--totp", "/Sample Entry"});
  1472. QCOMPARE(m_stdout->readLine(), QByteArray("Sample Entry\n"));
  1473. QVERIFY(isTotp(m_stdout->readAll()));
  1474. setInput("a");
  1475. execCmd(showCmd, {"show", m_dbFile2->fileName(), "--totp", "/Sample Entry"});
  1476. QCOMPARE(m_stdout->readAll(), QByteArray());
  1477. QVERIFY(m_stderr->readAll().contains("Entry with path /Sample Entry has no TOTP set up.\n"));
  1478. // Show with ambiguous attributes
  1479. setInput("a");
  1480. execCmd(showCmd, {"show", m_dbFile->fileName(), "-a", "Testattribute1", "/Sample Entry"});
  1481. QCOMPARE(m_stdout->readAll(), QByteArray());
  1482. QVERIFY(m_stderr->readAll().contains("ERROR: attribute Testattribute1 is ambiguous"));
  1483. }
  1484. void TestCli::testInvalidDbFiles()
  1485. {
  1486. Show showCmd;
  1487. QString nonExistentDbPath("/foo/bar/baz");
  1488. QString directoryName("/");
  1489. execCmd(showCmd, {"show", nonExistentDbPath, "/Sample Entry"});
  1490. QCOMPARE(QString(m_stderr->readAll()),
  1491. QObject::tr("Failed to open database file %1: not found").arg(nonExistentDbPath) + "\n");
  1492. QCOMPARE(m_stdout->readAll(), QByteArray());
  1493. execCmd(showCmd, {"show", directoryName, "whatever"});
  1494. QCOMPARE(QString(m_stderr->readAll()),
  1495. QObject::tr("Failed to open database file %1: not a plain file").arg(directoryName) + "\n");
  1496. // Create a write-only file and try to open it.
  1497. // QFileInfo.isReadable returns 'true' on Windows, even after the call to
  1498. // setPermissions(WriteOwner) and with NTFS permissions enabled, so this
  1499. // check doesn't work.
  1500. #if !defined(Q_OS_WIN)
  1501. QTemporaryFile tempFile;
  1502. QVERIFY(tempFile.open());
  1503. QString path = QFileInfo(tempFile).absoluteFilePath();
  1504. QVERIFY(tempFile.setPermissions(QFileDevice::WriteOwner));
  1505. execCmd(showCmd, {"show", path, "some entry"});
  1506. QCOMPARE(QString(m_stderr->readAll()),
  1507. QObject::tr("Failed to open database file %1: not readable").arg(path) + "\n");
  1508. #endif // Q_OS_WIN
  1509. }
  1510. /**
  1511. * Secret key for the YubiKey slot used by the unit test is
  1512. * 1c e3 0f d7 8d 20 dc fa 40 b5 0c 18 77 9a fb 0f 02 28 8d b7
  1513. * This secret can be on either slot but must be passive.
  1514. */
  1515. void TestCli::testYubiKeyOption()
  1516. {
  1517. if (!YubiKey::instance()->isInitialized()) {
  1518. QSKIP("Unable to initialize YubiKey interface.");
  1519. }
  1520. YubiKey::instance()->findValidKeys();
  1521. // Wait for the hardware to respond
  1522. QSignalSpy detected(YubiKey::instance(), SIGNAL(detectComplete(bool)));
  1523. QTRY_VERIFY_WITH_TIMEOUT(detected.count() > 0, 2000);
  1524. auto keys = YubiKey::instance()->foundKeys();
  1525. if (keys.isEmpty()) {
  1526. QSKIP("No YubiKey devices were detected.");
  1527. }
  1528. bool wouldBlock = false;
  1529. QByteArray challenge("CLITest");
  1530. Botan::secure_vector<char> response;
  1531. QByteArray expected("\xA2\x3B\x94\x00\xBE\x47\x9A\x30\xA9\xEB\x50\x9B\x85\x56\x5B\x6B\x30\x25\xB4\x8E", 20);
  1532. // Find a key that as configured for this test
  1533. YubiKeySlot pKey(0, 0);
  1534. for (auto key : keys) {
  1535. if (YubiKey::instance()->testChallenge(key, &wouldBlock) && !wouldBlock) {
  1536. YubiKey::instance()->challenge(key, challenge, response);
  1537. if (std::memcmp(response.data(), expected.data(), expected.size())) {
  1538. pKey = key;
  1539. break;
  1540. }
  1541. Tools::wait(100);
  1542. }
  1543. }
  1544. if (pKey.first == 0 && pKey.second == 0) {
  1545. QSKIP("No YubiKey is properly configured to perform this test.");
  1546. }
  1547. List listCmd;
  1548. Add addCmd;
  1549. setInput("a");
  1550. execCmd(listCmd, {"ls", "-y", "2", m_yubiKeyProtectedDbFile->fileName()});
  1551. m_stderr->readLine(); // skip password prompt
  1552. QCOMPARE(m_stderr->readAll(), QByteArray());
  1553. QCOMPARE(m_stdout->readAll(),
  1554. QByteArray("entry1\n"
  1555. "entry2\n"));
  1556. // Should raise an error with no yubikey slot.
  1557. setInput("a");
  1558. execCmd(listCmd, {"ls", m_yubiKeyProtectedDbFile->fileName()});
  1559. m_stderr->readLine(); // skip password prompt
  1560. QCOMPARE(m_stderr->readLine(),
  1561. QByteArray("Error while reading the database: Invalid credentials were provided, please try again.\n"));
  1562. QCOMPARE(m_stderr->readLine(),
  1563. QByteArray("If this reoccurs, then your database file may be corrupt. (HMAC mismatch)\n"));
  1564. QCOMPARE(m_stdout->readAll(), QByteArray());
  1565. // Should raise an error if yubikey slot is not a string
  1566. setInput("a");
  1567. execCmd(listCmd, {"ls", "-y", "invalidslot", m_yubiKeyProtectedDbFile->fileName()});
  1568. m_stderr->readLine(); // skip password prompt
  1569. QCOMPARE(m_stderr->readAll().split(':').at(0), QByteArray("Invalid YubiKey slot invalidslot\n"));
  1570. QCOMPARE(m_stdout->readAll(), QByteArray());
  1571. // Should raise an error if yubikey slot is invalid.
  1572. setInput("a");
  1573. execCmd(listCmd, {"ls", "-y", "3", m_yubiKeyProtectedDbFile->fileName()});
  1574. m_stderr->readLine(); // skip password prompt
  1575. QCOMPARE(m_stderr->readAll().split(':').at(0), QByteArray("Invalid YubiKey slot 3\n"));
  1576. QCOMPARE(m_stdout->readAll(), QByteArray());
  1577. }
  1578. void TestCli::testCommandParsing_data()
  1579. {
  1580. QTest::addColumn<QString>("input");
  1581. QTest::addColumn<QStringList>("expectedOutput");
  1582. QTest::newRow("basic") << "hello world" << QStringList({"hello", "world"});
  1583. QTest::newRow("basic escaping") << "hello\\ world" << QStringList({"hello world"});
  1584. QTest::newRow("quoted string") << "\"hello world\"" << QStringList({"hello world"});
  1585. QTest::newRow("multiple params") << "show Passwords/Internet" << QStringList({"show", "Passwords/Internet"});
  1586. QTest::newRow("quoted string inside param")
  1587. << R"(ls foo\ bar\ baz"quoted")" << QStringList({"ls", "foo bar baz\"quoted\""});
  1588. QTest::newRow("multiple whitespace") << "hello world" << QStringList({"hello", "world"});
  1589. QTest::newRow("single slash char") << "\\" << QStringList({"\\"});
  1590. QTest::newRow("double backslash entry name") << "show foo\\\\\\\\bar" << QStringList({"show", "foo\\\\bar"});
  1591. }
  1592. void TestCli::testCommandParsing()
  1593. {
  1594. QFETCH(QString, input);
  1595. QFETCH(QStringList, expectedOutput);
  1596. QStringList result = Utils::splitCommandString(input);
  1597. QCOMPARE(result.size(), expectedOutput.size());
  1598. for (int i = 0; i < expectedOutput.size(); ++i) {
  1599. QCOMPARE(result[i], expectedOutput[i]);
  1600. }
  1601. }
  1602. void TestCli::testOpen()
  1603. {
  1604. Open openCmd;
  1605. setInput("a");
  1606. execCmd(openCmd, {"open", m_dbFile->fileName()});
  1607. QVERIFY(openCmd.currentDatabase);
  1608. List listCmd;
  1609. // Set a current database, simulating interactive mode.
  1610. listCmd.currentDatabase = openCmd.currentDatabase;
  1611. execCmd(listCmd, {"ls"});
  1612. QByteArray expectedOutput("Sample Entry\n"
  1613. "General/\n"
  1614. "Windows/\n"
  1615. "Network/\n"
  1616. "Internet/\n"
  1617. "eMail/\n"
  1618. "Homebanking/\n");
  1619. QByteArray actualOutput = m_stdout->readAll();
  1620. actualOutput.truncate(expectedOutput.length());
  1621. QCOMPARE(actualOutput, expectedOutput);
  1622. }
  1623. void TestCli::testHelp()
  1624. {
  1625. Help helpCmd;
  1626. Commands::setupCommands(false);
  1627. execCmd(helpCmd, {"help"});
  1628. QVERIFY(m_stdout->readAll().contains("Available commands"));
  1629. List listCmd;
  1630. execCmd(helpCmd, {"help", "ls"});
  1631. QVERIFY(m_stdout->readAll().contains(listCmd.description.toLatin1()));
  1632. }