TestCli.cpp 93 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410
  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/Group.h"
  22. #include "core/Metadata.h"
  23. #include "core/Tools.h"
  24. #include "crypto/Crypto.h"
  25. #include "keys/FileKey.h"
  26. #include "keys/drivers/YubiKey.h"
  27. #include "cli/Add.h"
  28. #include "cli/AddGroup.h"
  29. #include "cli/Analyze.h"
  30. #include "cli/AttachmentExport.h"
  31. #include "cli/AttachmentImport.h"
  32. #include "cli/AttachmentRemove.h"
  33. #include "cli/Clip.h"
  34. #include "cli/DatabaseCreate.h"
  35. #include "cli/DatabaseEdit.h"
  36. #include "cli/DatabaseInfo.h"
  37. #include "cli/Diceware.h"
  38. #include "cli/Edit.h"
  39. #include "cli/Estimate.h"
  40. #include "cli/Export.h"
  41. #include "cli/Generate.h"
  42. #include "cli/Help.h"
  43. #include "cli/Import.h"
  44. #include "cli/List.h"
  45. #include "cli/Merge.h"
  46. #include "cli/Move.h"
  47. #include "cli/Open.h"
  48. #include "cli/Remove.h"
  49. #include "cli/RemoveGroup.h"
  50. #include "cli/Search.h"
  51. #include "cli/Show.h"
  52. #include "cli/Utils.h"
  53. #include <QClipboard>
  54. #include <QSignalSpy>
  55. #include <QTest>
  56. #include <QtConcurrent>
  57. #include <zxcvbn.h>
  58. QTEST_MAIN(TestCli)
  59. void TestCli::initTestCase()
  60. {
  61. QVERIFY(Crypto::init());
  62. // Create temporary config file
  63. Config::createConfigFromFile(TemporaryFile::createTempConfigFile(), {});
  64. QLocale::setDefault(QLocale::c());
  65. Bootstrap::bootstrap();
  66. m_devNull.reset(new QFile());
  67. #ifdef Q_OS_WIN
  68. m_devNull->open(fopen("nul", "w"), QIODevice::WriteOnly);
  69. #else
  70. m_devNull->open(fopen("/dev/null", "w"), QIODevice::WriteOnly);
  71. #endif
  72. Utils::DEVNULL.setDevice(m_devNull.data());
  73. }
  74. void TestCli::init()
  75. {
  76. const auto file = QString(KEEPASSX_TEST_DATA_DIR).append("/%1");
  77. m_dbFile.reset(new TemporaryFile());
  78. m_dbFile->copyFromFile(file.arg("NewDatabase.kdbx"));
  79. m_dbFile2.reset(new TemporaryFile());
  80. m_dbFile2->copyFromFile(file.arg("NewDatabase2.kdbx"));
  81. m_dbFileMulti.reset(new TemporaryFile());
  82. m_dbFileMulti->copyFromFile(file.arg("NewDatabaseMulti.kdbx"));
  83. m_xmlFile.reset(new TemporaryFile());
  84. m_xmlFile->copyFromFile(file.arg("NewDatabase.xml"));
  85. m_keyFileProtectedDbFile.reset(new TemporaryFile());
  86. m_keyFileProtectedDbFile->copyFromFile(file.arg("KeyFileProtected.kdbx"));
  87. m_keyFileProtectedNoPasswordDbFile.reset(new TemporaryFile());
  88. m_keyFileProtectedNoPasswordDbFile->copyFromFile(file.arg("KeyFileProtectedNoPassword.kdbx"));
  89. m_yubiKeyProtectedDbFile.reset(new TemporaryFile());
  90. m_yubiKeyProtectedDbFile->copyFromFile(file.arg("YubiKeyProtectedPasswords.kdbx"));
  91. m_nonAsciiDbFile.reset(new TemporaryFile());
  92. m_nonAsciiDbFile->copyFromFile(file.arg("NonAscii.kdbx"));
  93. m_stdout.reset(new QBuffer());
  94. m_stdout->open(QIODevice::ReadWrite);
  95. Utils::STDOUT.setDevice(m_stdout.data());
  96. m_stderr.reset(new QBuffer());
  97. m_stderr->open(QIODevice::ReadWrite);
  98. Utils::STDERR.setDevice(m_stderr.data());
  99. m_stdin.reset(new QBuffer());
  100. m_stdin->open(QIODevice::ReadWrite);
  101. Utils::STDIN.setDevice(m_stdin.data());
  102. }
  103. void TestCli::cleanup()
  104. {
  105. m_dbFile.reset();
  106. m_dbFile2.reset();
  107. m_dbFileMulti.reset();
  108. m_keyFileProtectedDbFile.reset();
  109. m_keyFileProtectedNoPasswordDbFile.reset();
  110. m_yubiKeyProtectedDbFile.reset();
  111. Utils::STDOUT.setDevice(nullptr);
  112. Utils::STDERR.setDevice(nullptr);
  113. Utils::STDIN.setDevice(nullptr);
  114. }
  115. void TestCli::cleanupTestCase()
  116. {
  117. m_devNull.reset();
  118. }
  119. QSharedPointer<Database> TestCli::readDatabase(const QString& filename, const QString& pw, const QString& keyfile)
  120. {
  121. auto db = QSharedPointer<Database>::create();
  122. auto key = QSharedPointer<CompositeKey>::create();
  123. if (filename.isEmpty()) {
  124. // Open the default test database
  125. key->addKey(QSharedPointer<PasswordKey>::create("a"));
  126. if (!db->open(m_dbFile->fileName(), key)) {
  127. return {};
  128. }
  129. } else {
  130. // Open the specified database file using supplied credentials
  131. key->addKey(QSharedPointer<PasswordKey>::create(pw));
  132. if (!keyfile.isEmpty()) {
  133. auto filekey = QSharedPointer<FileKey>::create();
  134. filekey->load(keyfile);
  135. key->addKey(filekey);
  136. }
  137. if (!db->open(filename, key)) {
  138. return {};
  139. }
  140. }
  141. return db;
  142. }
  143. int TestCli::execCmd(Command& cmd, const QStringList& args) const
  144. {
  145. // Move to end of stream
  146. m_stdout->readAll();
  147. m_stderr->readAll();
  148. // Record stream position
  149. auto outPos = m_stdout->pos();
  150. auto errPos = m_stderr->pos();
  151. // Execute command
  152. int ret = cmd.execute(args);
  153. // Move back to recorded position
  154. m_stdout->seek(outPos);
  155. m_stderr->seek(errPos);
  156. // Skip over blank lines
  157. QByteArray newline("\n");
  158. while (m_stdout->peek(1) == newline) {
  159. m_stdout->readLine();
  160. }
  161. while (m_stderr->peek(1) == newline) {
  162. m_stderr->readLine();
  163. }
  164. return ret;
  165. }
  166. bool TestCli::isTotp(const QString& value)
  167. {
  168. static const QRegularExpression totp("^\\d{6}$");
  169. return totp.match(value.trimmed()).hasMatch();
  170. }
  171. void TestCli::setInput(const QString& input)
  172. {
  173. setInput(QStringList(input));
  174. }
  175. void TestCli::setInput(const QStringList& input)
  176. {
  177. auto ba = input.join("\n").toLatin1();
  178. // Always end in newline
  179. if (!ba.endsWith("\n")) {
  180. ba.append("\n");
  181. }
  182. auto pos = m_stdin->pos();
  183. m_stdin->write(ba);
  184. m_stdin->seek(pos);
  185. }
  186. void TestCli::testBatchCommands()
  187. {
  188. Commands::setupCommands(false);
  189. QVERIFY(Commands::getCommand("add"));
  190. QVERIFY(Commands::getCommand("analyze"));
  191. QVERIFY(Commands::getCommand("attachment-export"));
  192. QVERIFY(Commands::getCommand("attachment-import"));
  193. QVERIFY(Commands::getCommand("attachment-rm"));
  194. QVERIFY(Commands::getCommand("clip"));
  195. QVERIFY(Commands::getCommand("close"));
  196. QVERIFY(Commands::getCommand("db-create"));
  197. QVERIFY(Commands::getCommand("db-info"));
  198. QVERIFY(Commands::getCommand("diceware"));
  199. QVERIFY(Commands::getCommand("edit"));
  200. QVERIFY(Commands::getCommand("estimate"));
  201. QVERIFY(Commands::getCommand("export"));
  202. QVERIFY(Commands::getCommand("generate"));
  203. QVERIFY(Commands::getCommand("help"));
  204. QVERIFY(Commands::getCommand("import"));
  205. QVERIFY(Commands::getCommand("ls"));
  206. QVERIFY(Commands::getCommand("merge"));
  207. QVERIFY(Commands::getCommand("mkdir"));
  208. QVERIFY(Commands::getCommand("mv"));
  209. QVERIFY(Commands::getCommand("open"));
  210. QVERIFY(Commands::getCommand("rm"));
  211. QVERIFY(Commands::getCommand("rmdir"));
  212. QVERIFY(Commands::getCommand("show"));
  213. QVERIFY(Commands::getCommand("search"));
  214. QVERIFY(!Commands::getCommand("doesnotexist"));
  215. QCOMPARE(Commands::getCommands().size(), 26);
  216. }
  217. void TestCli::testInteractiveCommands()
  218. {
  219. Commands::setupCommands(true);
  220. QVERIFY(Commands::getCommand("add"));
  221. QVERIFY(Commands::getCommand("analyze"));
  222. QVERIFY(Commands::getCommand("attachment-export"));
  223. QVERIFY(Commands::getCommand("attachment-import"));
  224. QVERIFY(Commands::getCommand("attachment-rm"));
  225. QVERIFY(Commands::getCommand("clip"));
  226. QVERIFY(Commands::getCommand("close"));
  227. QVERIFY(Commands::getCommand("db-create"));
  228. QVERIFY(Commands::getCommand("db-info"));
  229. QVERIFY(Commands::getCommand("diceware"));
  230. QVERIFY(Commands::getCommand("edit"));
  231. QVERIFY(Commands::getCommand("estimate"));
  232. QVERIFY(Commands::getCommand("exit"));
  233. QVERIFY(Commands::getCommand("generate"));
  234. QVERIFY(Commands::getCommand("help"));
  235. QVERIFY(Commands::getCommand("ls"));
  236. QVERIFY(Commands::getCommand("merge"));
  237. QVERIFY(Commands::getCommand("mkdir"));
  238. QVERIFY(Commands::getCommand("mv"));
  239. QVERIFY(Commands::getCommand("open"));
  240. QVERIFY(Commands::getCommand("quit"));
  241. QVERIFY(Commands::getCommand("rm"));
  242. QVERIFY(Commands::getCommand("rmdir"));
  243. QVERIFY(Commands::getCommand("show"));
  244. QVERIFY(Commands::getCommand("search"));
  245. QVERIFY(!Commands::getCommand("doesnotexist"));
  246. QCOMPARE(Commands::getCommands().size(), 26);
  247. }
  248. void TestCli::testAdd()
  249. {
  250. Add addCmd;
  251. QVERIFY(!addCmd.name.isEmpty());
  252. QVERIFY(addCmd.getDescriptionLine().contains(addCmd.name));
  253. setInput("a");
  254. execCmd(addCmd,
  255. {"add",
  256. "-u",
  257. "newuser",
  258. "--url",
  259. "https://example.com/",
  260. "-g",
  261. "-L",
  262. "20",
  263. "--notes",
  264. "some notes",
  265. m_dbFile->fileName(),
  266. "/newuser-entry"});
  267. m_stderr->readLine(); // Skip password prompt
  268. QCOMPARE(m_stderr->readAll(), QByteArray());
  269. QVERIFY(m_stdout->readAll().contains("Successfully added entry newuser-entry."));
  270. auto db = readDatabase();
  271. auto* entry = db->rootGroup()->findEntryByPath("/newuser-entry");
  272. QVERIFY(entry);
  273. QCOMPARE(entry->username(), QString("newuser"));
  274. QCOMPARE(entry->url(), QString("https://example.com/"));
  275. QCOMPARE(entry->password().size(), 20);
  276. QCOMPARE(entry->notes(), QString("some notes"));
  277. // Quiet option
  278. setInput("a");
  279. execCmd(addCmd, {"add", "-q", "-u", "newuser", "-g", "-L", "20", m_dbFile->fileName(), "/newentry-quiet"});
  280. QCOMPARE(m_stderr->readAll(), QByteArray());
  281. QCOMPARE(m_stdout->readAll(), QByteArray());
  282. db = readDatabase();
  283. entry = db->rootGroup()->findEntryByPath("/newentry-quiet");
  284. QVERIFY(entry);
  285. QCOMPARE(entry->password().size(), 20);
  286. setInput({"a", "newpassword"});
  287. execCmd(addCmd,
  288. {"add", "-u", "newuser2", "--url", "https://example.net/", "-p", m_dbFile->fileName(), "/newuser-entry2"});
  289. QVERIFY(m_stdout->readAll().contains("Successfully added entry newuser-entry2."));
  290. db = readDatabase();
  291. entry = db->rootGroup()->findEntryByPath("/newuser-entry2");
  292. QVERIFY(entry);
  293. QCOMPARE(entry->username(), QString("newuser2"));
  294. QCOMPARE(entry->url(), QString("https://example.net/"));
  295. QCOMPARE(entry->password(), QString("newpassword"));
  296. // Password generation options
  297. setInput("a");
  298. execCmd(addCmd, {"add", "-u", "newuser3", "-g", "-L", "34", m_dbFile->fileName(), "/newuser-entry3"});
  299. QVERIFY(m_stdout->readAll().contains("Successfully added entry newuser-entry3."));
  300. db = readDatabase();
  301. entry = db->rootGroup()->findEntryByPath("/newuser-entry3");
  302. QVERIFY(entry);
  303. QCOMPARE(entry->username(), QString("newuser3"));
  304. QCOMPARE(entry->password().size(), 34);
  305. QRegularExpression defaultPasswordClassesRegex("^[a-zA-Z0-9]+$");
  306. QVERIFY(defaultPasswordClassesRegex.match(entry->password()).hasMatch());
  307. setInput("a");
  308. execCmd(addCmd,
  309. {"add",
  310. "-u",
  311. "newuser4",
  312. "-g",
  313. "-L",
  314. "20",
  315. "--every-group",
  316. "-s",
  317. "-n",
  318. "-U",
  319. "-l",
  320. m_dbFile->fileName(),
  321. "/newuser-entry4"});
  322. QVERIFY(m_stdout->readAll().contains("Successfully added entry newuser-entry4."));
  323. db = readDatabase();
  324. entry = db->rootGroup()->findEntryByPath("/newuser-entry4");
  325. QVERIFY(entry);
  326. QCOMPARE(entry->username(), QString("newuser4"));
  327. QCOMPARE(entry->password().size(), 20);
  328. QVERIFY(!defaultPasswordClassesRegex.match(entry->password()).hasMatch());
  329. setInput("a");
  330. execCmd(addCmd, {"add", "-u", "newuser5", "--notes", "test\\nnew line", m_dbFile->fileName(), "/newuser-entry5"});
  331. m_stderr->readLine(); // skip password prompt
  332. QCOMPARE(m_stderr->readAll(), QByteArray(""));
  333. QCOMPARE(m_stdout->readAll(), QByteArray("Successfully added entry newuser-entry5.\n"));
  334. db = readDatabase();
  335. entry = db->rootGroup()->findEntryByPath("/newuser-entry5");
  336. QVERIFY(entry);
  337. QCOMPARE(entry->username(), QString("newuser5"));
  338. QCOMPARE(entry->notes(), QString("test\nnew line"));
  339. }
  340. void TestCli::testAddGroup()
  341. {
  342. AddGroup addGroupCmd;
  343. QVERIFY(!addGroupCmd.name.isEmpty());
  344. QVERIFY(addGroupCmd.getDescriptionLine().contains(addGroupCmd.name));
  345. setInput("a");
  346. execCmd(addGroupCmd, {"mkdir", m_dbFile->fileName(), "/new_group"});
  347. m_stderr->readLine(); // Skip password prompt
  348. QCOMPARE(m_stderr->readAll(), QByteArray());
  349. QCOMPARE(m_stdout->readAll(), QByteArray("Successfully added group new_group.\n"));
  350. auto db = readDatabase();
  351. auto* group = db->rootGroup()->findGroupByPath("new_group");
  352. QVERIFY(group);
  353. QCOMPARE(group->name(), QString("new_group"));
  354. // Trying to add the same group should fail.
  355. setInput("a");
  356. execCmd(addGroupCmd, {"mkdir", m_dbFile->fileName(), "/new_group"});
  357. QVERIFY(m_stderr->readAll().contains("Group /new_group already exists!"));
  358. QCOMPARE(m_stdout->readAll(), QByteArray());
  359. // Should be able to add groups down the tree.
  360. setInput("a");
  361. execCmd(addGroupCmd, {"mkdir", m_dbFile->fileName(), "/new_group/newer_group"});
  362. QVERIFY(m_stdout->readAll().contains("Successfully added group newer_group."));
  363. db = readDatabase();
  364. group = db->rootGroup()->findGroupByPath("new_group/newer_group");
  365. QVERIFY(group);
  366. QCOMPARE(group->name(), QString("newer_group"));
  367. // Should fail if the path is invalid.
  368. setInput("a");
  369. execCmd(addGroupCmd, {"mkdir", m_dbFile->fileName(), "/invalid_group/newer_group"});
  370. QVERIFY(m_stderr->readAll().contains("Group /invalid_group not found."));
  371. QCOMPARE(m_stdout->readAll(), QByteArray());
  372. // Should fail to add the root group.
  373. setInput("a");
  374. execCmd(addGroupCmd, {"mkdir", m_dbFile->fileName(), "/"});
  375. QVERIFY(m_stderr->readAll().contains("Group / already exists!"));
  376. QCOMPARE(m_stdout->readAll(), QByteArray());
  377. }
  378. void TestCli::testAnalyze()
  379. {
  380. Analyze analyzeCmd;
  381. QVERIFY(!analyzeCmd.name.isEmpty());
  382. QVERIFY(analyzeCmd.getDescriptionLine().contains(analyzeCmd.name));
  383. const QString hibpPath = QString(KEEPASSX_TEST_DATA_DIR).append("/hibp.txt");
  384. setInput("a");
  385. execCmd(analyzeCmd, {"analyze", "--hibp", hibpPath, m_dbFile->fileName()});
  386. auto output = m_stdout->readAll();
  387. QVERIFY(output.contains("Sample Entry"));
  388. QVERIFY(output.contains("123"));
  389. m_stderr->readLine(); // Skip password prompt
  390. QCOMPARE(m_stderr->readAll(), QByteArray());
  391. }
  392. void TestCli::testAttachmentExport()
  393. {
  394. AttachmentExport attachmentExportCmd;
  395. QVERIFY(!attachmentExportCmd.name.isEmpty());
  396. QVERIFY(attachmentExportCmd.getDescriptionLine().contains(attachmentExportCmd.name));
  397. TemporaryFile exportOutput;
  398. exportOutput.open(QIODevice::WriteOnly);
  399. exportOutput.close();
  400. // Try exporting an attachment of a non-existent entry
  401. setInput("a");
  402. execCmd(attachmentExportCmd,
  403. {"attachment-export",
  404. m_dbFile->fileName(),
  405. "invalid_entry_path",
  406. "invalid_attachment_name",
  407. exportOutput.fileName()});
  408. m_stderr->readLine(); // skip password prompt
  409. QCOMPARE(m_stderr->readAll(), QByteArray("Could not find entry with path invalid_entry_path.\n"));
  410. QCOMPARE(m_stdout->readAll(), QByteArray());
  411. // Try exporting a non-existent attachment
  412. setInput("a");
  413. execCmd(attachmentExportCmd,
  414. {"attachment-export",
  415. m_dbFile->fileName(),
  416. "/Sample Entry",
  417. "invalid_attachment_name",
  418. exportOutput.fileName()});
  419. m_stderr->readLine(); // skip password prompt
  420. QCOMPARE(m_stderr->readAll(), QByteArray("Could not find attachment with name invalid_attachment_name.\n"));
  421. QCOMPARE(m_stdout->readAll(), QByteArray());
  422. // Export an existing attachment to a file
  423. setInput("a");
  424. execCmd(
  425. attachmentExportCmd,
  426. {"attachment-export", m_dbFile->fileName(), "/Sample Entry", "Sample attachment.txt", exportOutput.fileName()});
  427. m_stderr->readLine(); // skip password prompt
  428. QCOMPARE(m_stderr->readAll(), QByteArray());
  429. QCOMPARE(m_stdout->readAll(),
  430. QByteArray(qPrintable(QString("Successfully exported attachment %1 of entry %2 to %3.\n")
  431. .arg("Sample attachment.txt", "/Sample Entry", exportOutput.fileName()))));
  432. exportOutput.open(QIODevice::ReadOnly);
  433. QCOMPARE(exportOutput.readAll(), QByteArray("Sample content\n"));
  434. // Export an existing attachment to stdout
  435. setInput("a");
  436. execCmd(attachmentExportCmd,
  437. {"attachment-export", "--stdout", m_dbFile->fileName(), "/Sample Entry", "Sample attachment.txt"});
  438. m_stderr->readLine(); // skip password prompt
  439. QCOMPARE(m_stderr->readAll(), QByteArray());
  440. QCOMPARE(m_stdout->readAll(), QByteArray("Sample content\n"));
  441. // Ensure --stdout works even in quiet mode
  442. setInput("a");
  443. execCmd(
  444. attachmentExportCmd,
  445. {"attachment-export", "--quiet", "--stdout", m_dbFile->fileName(), "/Sample Entry", "Sample attachment.txt"});
  446. m_stderr->readLine(); // skip password prompt
  447. QCOMPARE(m_stderr->readAll(), QByteArray());
  448. QCOMPARE(m_stdout->readAll(), QByteArray("Sample content\n"));
  449. }
  450. void TestCli::testAttachmentImport()
  451. {
  452. AttachmentImport attachmentImportCmd;
  453. QVERIFY(!attachmentImportCmd.name.isEmpty());
  454. QVERIFY(attachmentImportCmd.getDescriptionLine().contains(attachmentImportCmd.name));
  455. const QString attachmentPath = QString(KEEPASSX_TEST_DATA_DIR).append("/Attachment.txt");
  456. QVERIFY(QFile::exists(attachmentPath));
  457. // Try importing an attachment to a non-existent entry
  458. setInput("a");
  459. execCmd(attachmentImportCmd,
  460. {"attachment-import",
  461. m_dbFile->fileName(),
  462. "invalid_entry_path",
  463. "invalid_attachment_name",
  464. "invalid_attachment_path"});
  465. m_stderr->readLine(); // skip password prompt
  466. QCOMPARE(m_stderr->readAll(), QByteArray("Could not find entry with path invalid_entry_path.\n"));
  467. QCOMPARE(m_stdout->readAll(), QByteArray());
  468. // Try importing an attachment with an occupied name without -f option
  469. setInput("a");
  470. execCmd(attachmentImportCmd,
  471. {"attachment-import",
  472. m_dbFile->fileName(),
  473. "/Sample Entry",
  474. "Sample attachment.txt",
  475. "invalid_attachment_path"});
  476. m_stderr->readLine(); // skip password prompt
  477. QCOMPARE(m_stderr->readAll(),
  478. QByteArray("Attachment Sample attachment.txt already exists for entry /Sample Entry.\n"));
  479. QCOMPARE(m_stdout->readAll(), QByteArray());
  480. // Try importing a non-existent attachment
  481. setInput("a");
  482. execCmd(attachmentImportCmd,
  483. {"attachment-import",
  484. m_dbFile->fileName(),
  485. "/Sample Entry",
  486. "Sample attachment 2.txt",
  487. "invalid_attachment_path"});
  488. m_stderr->readLine(); // skip password prompt
  489. QCOMPARE(m_stderr->readAll(), QByteArray("Could not open attachment file invalid_attachment_path.\n"));
  490. QCOMPARE(m_stdout->readAll(), QByteArray());
  491. // Try importing an attachment with an occupied name with -f option
  492. setInput("a");
  493. execCmd(
  494. attachmentImportCmd,
  495. {"attachment-import", "-f", m_dbFile->fileName(), "/Sample Entry", "Sample attachment.txt", attachmentPath});
  496. m_stderr->readLine(); // skip password prompt
  497. QCOMPARE(m_stderr->readAll(), QByteArray());
  498. QCOMPARE(m_stdout->readAll(),
  499. QByteArray(qPrintable(
  500. QString("Successfully imported attachment %1 as Sample attachment.txt to entry /Sample Entry.\n")
  501. .arg(attachmentPath))));
  502. // Try importing an attachment with an unoccupied name
  503. setInput("a");
  504. execCmd(attachmentImportCmd,
  505. {"attachment-import", m_dbFile->fileName(), "/Sample Entry", "Attachment.txt", attachmentPath});
  506. m_stderr->readLine(); // skip password prompt
  507. QCOMPARE(m_stderr->readAll(), QByteArray());
  508. QCOMPARE(
  509. m_stdout->readAll(),
  510. QByteArray(qPrintable(QString("Successfully imported attachment %1 as Attachment.txt to entry /Sample Entry.\n")
  511. .arg(attachmentPath))));
  512. }
  513. void TestCli::testAttachmentRemove()
  514. {
  515. AttachmentRemove attachmentRemoveCmd;
  516. QVERIFY(!attachmentRemoveCmd.name.isEmpty());
  517. QVERIFY(attachmentRemoveCmd.getDescriptionLine().contains(attachmentRemoveCmd.name));
  518. // Try deleting an attachment belonging to an non-existent entry
  519. setInput("a");
  520. execCmd(attachmentRemoveCmd,
  521. {"attachment-rm", m_dbFile->fileName(), "invalid_entry_path", "invalid_attachment_name"});
  522. m_stderr->readLine(); // skip password prompt
  523. QCOMPARE(m_stderr->readAll(), QByteArray("Could not find entry with path invalid_entry_path.\n"));
  524. QCOMPARE(m_stdout->readAll(), QByteArray());
  525. // Try deleting a non-existent attachment from an entry
  526. setInput("a");
  527. execCmd(attachmentRemoveCmd, {"attachment-rm", m_dbFile->fileName(), "/Sample Entry", "invalid_attachment_name"});
  528. m_stderr->readLine(); // skip password prompt
  529. QCOMPARE(m_stderr->readAll(), QByteArray("Could not find attachment with name invalid_attachment_name.\n"));
  530. QCOMPARE(m_stdout->readAll(), QByteArray());
  531. // Finally delete an existing attachment from an existing entry
  532. auto db = readDatabase();
  533. QVERIFY(db);
  534. const Entry* entry = db->rootGroup()->findEntryByPath("/Sample Entry");
  535. QVERIFY(entry);
  536. QVERIFY(entry->attachments()->hasKey("Sample attachment.txt"));
  537. setInput("a");
  538. execCmd(attachmentRemoveCmd, {"attachment-rm", m_dbFile->fileName(), "/Sample Entry", "Sample attachment.txt"});
  539. m_stderr->readLine(); // skip password prompt
  540. QCOMPARE(m_stderr->readAll(), QByteArray());
  541. QCOMPARE(m_stdout->readAll(),
  542. QByteArray("Successfully removed attachment Sample attachment.txt from entry /Sample Entry.\n"));
  543. db = readDatabase();
  544. QVERIFY(db);
  545. QVERIFY(!db->rootGroup()->findEntryByPath("/Sample Entry")->attachments()->hasKey("Sample attachment.txt"));
  546. }
  547. void TestCli::testClip()
  548. {
  549. if (QProcessEnvironment::systemEnvironment().contains("WAYLAND_DISPLAY")) {
  550. QSKIP("Clip test skipped due to QClipboard and Wayland issues on Linux");
  551. }
  552. QClipboard* clipboard = QGuiApplication::clipboard();
  553. clipboard->clear();
  554. Clip clipCmd;
  555. QVERIFY(!clipCmd.name.isEmpty());
  556. QVERIFY(clipCmd.getDescriptionLine().contains(clipCmd.name));
  557. // Password
  558. setInput("a");
  559. execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry", "0"});
  560. QString errorOutput(m_stderr->readAll());
  561. if (errorOutput.contains("Unable to start program")
  562. || errorOutput.contains("No program defined for clipboard manipulation")) {
  563. QSKIP("Clip test skipped due to missing clipboard tool");
  564. }
  565. QVERIFY(!errorOutput.contains("All clipping programs failed"));
  566. m_stderr->readLine(); // Skip password prompt
  567. QCOMPARE(m_stderr->readAll(), QByteArray());
  568. QTRY_COMPARE(clipboard->text(), QString("Password"));
  569. QCOMPARE(m_stdout->readLine(), QByteArray("Entry's \"Password\" attribute copied to the clipboard!\n"));
  570. // Quiet option
  571. setInput("a");
  572. execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry", "0", "-q"});
  573. QCOMPARE(m_stderr->readAll(), QByteArray());
  574. QCOMPARE(m_stdout->readAll(), QByteArray());
  575. QTRY_COMPARE(clipboard->text(), QString("Password"));
  576. // Username
  577. setInput("a");
  578. execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry", "0", "-a", "username"});
  579. QTRY_COMPARE(clipboard->text(), QString("User Name"));
  580. // Uuid (top-level field)
  581. setInput("a");
  582. execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry", "0", "-a", "Uuid"});
  583. QTRY_COMPARE(clipboard->text(), QString("{9f4544c2-ab00-c74a-8a1a-6eaf26cf57e9}"));
  584. // TOTP
  585. setInput("a");
  586. execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry", "0", "--totp"});
  587. QTRY_VERIFY(isTotp(clipboard->text()));
  588. QCOMPARE(m_stdout->readLine(), QByteArray("Entry's \"totp\" attribute copied to the clipboard!\n"));
  589. // Test Unicode
  590. setInput("a");
  591. execCmd(clipCmd, {"clip", m_dbFile2->fileName(), "/General/Unicode", "0", "-a", "username"});
  592. QTRY_COMPARE(clipboard->text(), QString(R"(¯\_(ツ)_/¯)"));
  593. // Password with timeout
  594. setInput("a");
  595. // clang-format off
  596. QFuture<void> future = QtConcurrent::run(&clipCmd,
  597. static_cast<int(Clip::*)(const QStringList&)>(&DatabaseCommand::execute),
  598. QStringList{"clip", m_dbFile->fileName(), "/Sample Entry", "1"});
  599. // clang-format on
  600. QTRY_COMPARE(clipboard->text(), QString("Password"));
  601. QTRY_COMPARE_WITH_TIMEOUT(clipboard->text(), QString(""), 3000);
  602. future.waitForFinished();
  603. // TOTP with timeout
  604. setInput("a");
  605. future = QtConcurrent::run(&clipCmd,
  606. static_cast<int (Clip::*)(const QStringList&)>(&DatabaseCommand::execute),
  607. QStringList{"clip", m_dbFile->fileName(), "/Sample Entry", "1", "-t"});
  608. QTRY_VERIFY(isTotp(clipboard->text()));
  609. QTRY_COMPARE_WITH_TIMEOUT(clipboard->text(), QString(""), 3000);
  610. future.waitForFinished();
  611. setInput("a");
  612. execCmd(clipCmd, {"clip", m_dbFile->fileName(), "--totp", "/Sample Entry", "bleuh"});
  613. QVERIFY(m_stderr->readAll().contains("Invalid timeout value bleuh.\n"));
  614. setInput("a");
  615. execCmd(clipCmd, {"clip", m_dbFile2->fileName(), "--totp", "/Sample Entry", "0"});
  616. QVERIFY(m_stderr->readAll().contains("Entry with path /Sample Entry has no TOTP set up.\n"));
  617. setInput("a");
  618. execCmd(clipCmd, {"clip", m_dbFile->fileName(), "-a", "TESTAttribute1", "/Sample Entry", "0"});
  619. QVERIFY(m_stderr->readAll().contains("ERROR: attribute TESTAttribute1 is ambiguous"));
  620. setInput("a");
  621. execCmd(clipCmd, {"clip", m_dbFile2->fileName(), "--attribute", "Username", "--totp", "/Sample Entry", "0"});
  622. QVERIFY(m_stderr->readAll().contains("ERROR: Please specify one of --attribute or --totp, not both.\n"));
  623. // Best option
  624. setInput("a");
  625. execCmd(clipCmd, {"clip", m_dbFileMulti->fileName(), "Multi", "0", "-b"});
  626. QByteArray errorChoices = m_stderr->readAll();
  627. QVERIFY(errorChoices.contains("Multi Entry 1"));
  628. QVERIFY(errorChoices.contains("Multi Entry 2"));
  629. setInput("a");
  630. execCmd(clipCmd, {"clip", m_dbFileMulti->fileName(), "Entry 2", "0", "-b"});
  631. QTRY_COMPARE(clipboard->text(), QString("Password2"));
  632. }
  633. void TestCli::testCreate()
  634. {
  635. DatabaseCreate createCmd;
  636. QVERIFY(!createCmd.name.isEmpty());
  637. QVERIFY(createCmd.getDescriptionLine().contains(createCmd.name));
  638. QScopedPointer<QTemporaryDir> testDir(new QTemporaryDir());
  639. QString dbFilename;
  640. // Testing password option, password mismatch
  641. dbFilename = testDir->path() + "/testCreate_pw.kdbx";
  642. setInput({"a", "b"});
  643. execCmd(createCmd, {"db-create", dbFilename, "-p"});
  644. QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
  645. QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
  646. QCOMPARE(m_stderr->readLine(), QByteArray("Error: Passwords do not match.\n"));
  647. QCOMPARE(m_stderr->readLine(), QByteArray("Failed to set database password.\n"));
  648. // Testing password option
  649. setInput({"a", "a"});
  650. execCmd(createCmd, {"db-create", dbFilename, "-p"});
  651. QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
  652. QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
  653. QCOMPARE(m_stdout->readLine(), QByteArray("Successfully created new database.\n"));
  654. auto db = readDatabase(dbFilename, "a");
  655. QVERIFY(db);
  656. // Testing with empty password (deny it)
  657. dbFilename = testDir->path() + "/testCreate_blankpw.kdbx";
  658. setInput({"", "n"});
  659. execCmd(createCmd, {"db-create", dbFilename, "-p"});
  660. QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
  661. QVERIFY(m_stderr->readLine().contains("empty password"));
  662. QCOMPARE(m_stderr->readLine(), QByteArray("Failed to set database password.\n"));
  663. // Testing with empty password (accept it)
  664. setInput({"", "y"});
  665. execCmd(createCmd, {"db-create", dbFilename, "-p"});
  666. QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
  667. QVERIFY(m_stderr->readLine().contains("empty password"));
  668. QCOMPARE(m_stdout->readLine(), QByteArray("Successfully created new database.\n"));
  669. db = readDatabase(dbFilename, "");
  670. QVERIFY(db);
  671. // Should refuse to create the database if it already exists.
  672. execCmd(createCmd, {"db-create", dbFilename, "-p"});
  673. // Output should be empty when there is an error.
  674. QCOMPARE(m_stdout->readAll(), QByteArray());
  675. QString errorMessage = QString("File " + dbFilename + " already exists.\n");
  676. QCOMPARE(m_stderr->readAll(), errorMessage.toUtf8());
  677. // Should refuse to create without any key provided.
  678. dbFilename = testDir->path() + "/testCreate_key.kdbx";
  679. execCmd(createCmd, {"db-create", dbFilename});
  680. QCOMPARE(m_stdout->readAll(), QByteArray());
  681. QCOMPARE(m_stderr->readLine(), QByteArray("No key is set. Aborting database creation.\n"));
  682. // Testing with keyfile creation
  683. dbFilename = testDir->path() + "/testCreate_key2.kdbx";
  684. QString keyfilePath = testDir->path() + "/keyfile.txt";
  685. setInput({"a", "a"});
  686. execCmd(createCmd, {"db-create", dbFilename, "-p", "-k", keyfilePath});
  687. QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
  688. QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
  689. QCOMPARE(m_stdout->readLine(), QByteArray("Successfully created new database.\n"));
  690. db = readDatabase(dbFilename, "a", keyfilePath);
  691. QVERIFY(db);
  692. // Testing with existing keyfile
  693. dbFilename = testDir->path() + "/testCreate_key3.kdbx";
  694. setInput({"a", "a"});
  695. execCmd(createCmd, {"db-create", dbFilename, "-p", "-k", keyfilePath});
  696. QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
  697. QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
  698. QCOMPARE(m_stdout->readLine(), QByteArray("Successfully created new database.\n"));
  699. db = readDatabase(dbFilename, "a", keyfilePath);
  700. QVERIFY(db);
  701. // Invalid decryption time (format).
  702. dbFilename = testDir->path() + "/testCreate_time.kdbx";
  703. execCmd(createCmd, {"db-create", dbFilename, "-p", "-t", "NAN"});
  704. QCOMPARE(m_stdout->readAll(), QByteArray());
  705. QCOMPARE(m_stderr->readAll(), QByteArray("Invalid decryption time NAN.\n"));
  706. // Invalid decryption time (range).
  707. execCmd(createCmd, {"db-create", dbFilename, "-p", "-t", "10"});
  708. QCOMPARE(m_stdout->readAll(), QByteArray());
  709. QVERIFY(m_stderr->readAll().contains(QByteArray("Target decryption time must be between")));
  710. int encryptionTime = 500;
  711. // Custom encryption time
  712. setInput({"a", "a"});
  713. int epochBefore = QDateTime::currentMSecsSinceEpoch();
  714. execCmd(createCmd, {"db-create", dbFilename, "-p", "-t", QString::number(encryptionTime)});
  715. // Removing 100ms to make sure we account for changes in computation time.
  716. QVERIFY(QDateTime::currentMSecsSinceEpoch() > (epochBefore + encryptionTime - 100));
  717. QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
  718. QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
  719. QCOMPARE(m_stdout->readLine(), QByteArray("Benchmarking key derivation function for 500ms delay.\n"));
  720. QVERIFY(m_stdout->readLine().contains(QByteArray("rounds for key derivation function.\n")));
  721. db = readDatabase(dbFilename, "a");
  722. QVERIFY(db);
  723. }
  724. void TestCli::testDatabaseEdit()
  725. {
  726. TemporaryFile firstKeyFile;
  727. firstKeyFile.open();
  728. firstKeyFile.write(QString("keyFilePassword").toLatin1());
  729. firstKeyFile.close();
  730. TemporaryFile secondKeyFile;
  731. secondKeyFile.open();
  732. secondKeyFile.write(QString("newKeyFilePassword").toLatin1());
  733. secondKeyFile.close();
  734. QScopedPointer<QTemporaryDir> testDir(new QTemporaryDir());
  735. DatabaseCreate createCmd;
  736. DatabaseEdit editCmd;
  737. QVERIFY(!editCmd.name.isEmpty());
  738. QVERIFY(editCmd.getDescriptionLine().contains(editCmd.name));
  739. QString dbFilename;
  740. dbFilename = testDir->path() + "/testDatabaseEdit.kdbx";
  741. // Creating a database for testing
  742. setInput({"a", "a"});
  743. execCmd(createCmd, {"db-create", dbFilename, "-p"});
  744. QCOMPARE(m_stdout->readLine(), QByteArray("Successfully created new database.\n"));
  745. // Sanity check.
  746. auto db = readDatabase(dbFilename, "a");
  747. QVERIFY(!db.isNull());
  748. setInput("a");
  749. execCmd(editCmd, {"db-edit", dbFilename, "-p", "--unset-password"});
  750. QCOMPARE(m_stdout->readAll(), QByteArray(""));
  751. m_stderr->readLine();
  752. QCOMPARE(m_stderr->readAll(), QByteArray("Cannot use p and unset-password at the same time.\n"));
  753. setInput("a");
  754. execCmd(editCmd, {"db-edit", dbFilename, "--set-key-file", "/key/file/path", "--unset-key-file"});
  755. QCOMPARE(m_stdout->readAll(), QByteArray(""));
  756. // Skipping the password prompt.
  757. m_stderr->readLine();
  758. QCOMPARE(m_stderr->readAll(), QByteArray("Cannot use set-key-file and unset-key-file at the same time.\n"));
  759. // Sanity check.
  760. db = readDatabase(dbFilename, "a");
  761. QVERIFY(!db.isNull());
  762. setInput({"a", "b", "b"});
  763. execCmd(editCmd, {"db-edit", dbFilename, "-p"});
  764. QCOMPARE(m_stdout->readAll(), QByteArray("Successfully edited the database.\n"));
  765. // Sanity check
  766. db = readDatabase(dbFilename, "b");
  767. QVERIFY(!db.isNull());
  768. setInput("b");
  769. execCmd(editCmd, {"db-edit", dbFilename, "--set-key-file", firstKeyFile.fileName()});
  770. // Skipping the password prompt.
  771. m_stderr->readLine();
  772. QCOMPARE(m_stderr->readAll(), QByteArray(""));
  773. QCOMPARE(m_stdout->readAll(), QByteArray("Successfully edited the database.\n"));
  774. // Sanity check
  775. db = readDatabase(dbFilename, "b");
  776. QVERIFY(db.isNull());
  777. db = readDatabase(dbFilename, "b", firstKeyFile.fileName());
  778. QVERIFY(!db.isNull());
  779. setInput("b");
  780. execCmd(editCmd,
  781. {"db-edit", dbFilename, "-k", firstKeyFile.fileName(), "--set-key-file", secondKeyFile.fileName()});
  782. QCOMPARE(m_stdout->readAll(), QByteArray("Successfully edited the database.\n"));
  783. // Sanity check
  784. db = readDatabase(dbFilename, "b", firstKeyFile.fileName());
  785. QVERIFY(db.isNull());
  786. db = readDatabase(dbFilename, "b", secondKeyFile.fileName());
  787. QVERIFY(!db.isNull());
  788. setInput("b");
  789. execCmd(editCmd, {"db-edit", dbFilename, "-k", secondKeyFile.fileName(), "--unset-password"});
  790. // Skipping the password prompt.
  791. m_stderr->readLine();
  792. QCOMPARE(m_stderr->readAll(), QByteArray(""));
  793. QCOMPARE(m_stdout->readAll(), QByteArray("Successfully edited the database.\n"));
  794. execCmd(editCmd,
  795. {"db-edit",
  796. dbFilename,
  797. "--no-password",
  798. "-k",
  799. secondKeyFile.fileName(),
  800. "--set-key-file",
  801. firstKeyFile.fileName()});
  802. // Skipping the password prompt.
  803. m_stderr->readLine();
  804. QCOMPARE(m_stderr->readAll(), QByteArray(""));
  805. QCOMPARE(m_stdout->readAll(), QByteArray("Successfully edited the database.\n"));
  806. setInput({"b", "b"});
  807. execCmd(editCmd, {"db-edit", dbFilename, "-k", firstKeyFile.fileName(), "--no-password", "--set-password"});
  808. // Skipping over the password setting prompts.
  809. m_stderr->readLine();
  810. m_stderr->readLine();
  811. QCOMPARE(m_stderr->readAll(), QByteArray(""));
  812. QCOMPARE(m_stdout->readAll(), QByteArray("Successfully edited the database.\n"));
  813. setInput("b");
  814. execCmd(editCmd, {"db-edit", dbFilename, "-k", firstKeyFile.fileName(), "--unset-key-file"});
  815. // Skipping the password prompt.
  816. m_stderr->readLine();
  817. QCOMPARE(m_stderr->readAll(), QByteArray(""));
  818. QCOMPARE(m_stdout->readAll(), QByteArray("Successfully edited the database.\n"));
  819. // Sanity check
  820. db = readDatabase(dbFilename, "b", firstKeyFile.fileName());
  821. QVERIFY(db.isNull());
  822. db = readDatabase(dbFilename, "b");
  823. QVERIFY(!db.isNull());
  824. // Trying to remove the key file when there is none set should
  825. // raise an error.
  826. setInput("b");
  827. execCmd(editCmd, {"db-edit", dbFilename, "-p", "--unset-key-file"});
  828. QCOMPARE(m_stdout->readAll(), QByteArray(""));
  829. m_stderr->readLine();
  830. QCOMPARE(m_stderr->readLine(), QByteArray("Cannot remove file key: The database does not have a file key.\n"));
  831. QCOMPARE(m_stderr->readLine(), QByteArray("Could not change the database key.\n"));
  832. setInput("b");
  833. execCmd(editCmd, {"db-edit", dbFilename, "--unset-password"});
  834. QCOMPARE(m_stdout->readAll(), QByteArray(""));
  835. // Skipping the password prompt.
  836. m_stderr->readLine();
  837. QCOMPARE(m_stderr->readLine(), QByteArray("Cannot remove all the keys from a database.\n"));
  838. }
  839. void TestCli::testInfo()
  840. {
  841. DatabaseInfo infoCmd;
  842. QVERIFY(!infoCmd.name.isEmpty());
  843. QVERIFY(infoCmd.getDescriptionLine().contains(infoCmd.name));
  844. setInput("a");
  845. execCmd(infoCmd, {"db-info", m_dbFile->fileName()});
  846. m_stderr->readLine(); // Skip password prompt
  847. QCOMPARE(m_stderr->readAll(), QByteArray());
  848. QVERIFY(m_stdout->readLine().contains(QByteArray("UUID: ")));
  849. QCOMPARE(m_stdout->readLine(), QByteArray("Name: \n"));
  850. QCOMPARE(m_stdout->readLine(), QByteArray("Description: \n"));
  851. QCOMPARE(m_stdout->readLine(), QByteArray("Cipher: AES 256-bit\n"));
  852. QCOMPARE(m_stdout->readLine(), QByteArray("KDF: AES (6000 rounds)\n"));
  853. QCOMPARE(m_stdout->readLine(), QByteArray("Recycle bin is enabled.\n"));
  854. QVERIFY(m_stdout->readLine().contains(m_dbFile->fileName().toUtf8()));
  855. QVERIFY(m_stdout->readLine().contains(
  856. QByteArray("Database created: "))); // date changes often, so just test for the first part
  857. QVERIFY(m_stdout->readLine().contains(
  858. QByteArray("Last saved: "))); // date changes often, so just test for the first part
  859. QCOMPARE(m_stdout->readLine(), QByteArray("Unsaved changes: no\n"));
  860. QCOMPARE(m_stdout->readLine(), QByteArray("Number of groups: 8\n"));
  861. QCOMPARE(m_stdout->readLine(), QByteArray("Number of entries: 2\n"));
  862. QCOMPARE(m_stdout->readLine(), QByteArray("Number of expired entries: 0\n"));
  863. QCOMPARE(m_stdout->readLine(), QByteArray("Unique passwords: 2\n"));
  864. QCOMPARE(m_stdout->readLine(), QByteArray("Non-unique passwords: 0\n"));
  865. QCOMPARE(m_stdout->readLine(), QByteArray("Maximum password reuse: 1\n"));
  866. QCOMPARE(m_stdout->readLine(), QByteArray("Number of short passwords: 0\n"));
  867. QCOMPARE(m_stdout->readLine(), QByteArray("Number of weak passwords: 2\n"));
  868. QCOMPARE(m_stdout->readLine(), QByteArray("Entries excluded from reports: 0\n"));
  869. QCOMPARE(m_stdout->readLine(), QByteArray("Average password length: 11 characters\n"));
  870. // Test with quiet option.
  871. setInput("a");
  872. execCmd(infoCmd, {"db-info", "-q", m_dbFile->fileName()});
  873. QCOMPARE(m_stderr->readAll(), QByteArray());
  874. QVERIFY(m_stdout->readLine().contains(QByteArray("UUID: ")));
  875. QCOMPARE(m_stdout->readLine(), QByteArray("Name: \n"));
  876. QCOMPARE(m_stdout->readLine(), QByteArray("Description: \n"));
  877. QCOMPARE(m_stdout->readLine(), QByteArray("Cipher: AES 256-bit\n"));
  878. QCOMPARE(m_stdout->readLine(), QByteArray("KDF: AES (6000 rounds)\n"));
  879. QCOMPARE(m_stdout->readLine(), QByteArray("Recycle bin is enabled.\n"));
  880. }
  881. void TestCli::testDiceware()
  882. {
  883. Diceware dicewareCmd;
  884. QVERIFY(!dicewareCmd.name.isEmpty());
  885. QVERIFY(dicewareCmd.getDescriptionLine().contains(dicewareCmd.name));
  886. execCmd(dicewareCmd, {"diceware"});
  887. QString passphrase(m_stdout->readLine());
  888. QVERIFY(!passphrase.isEmpty());
  889. execCmd(dicewareCmd, {"diceware", "-W", "2"});
  890. passphrase = m_stdout->readLine();
  891. QCOMPARE(passphrase.split(" ").size(), 2);
  892. execCmd(dicewareCmd, {"diceware", "-W", "10"});
  893. passphrase = m_stdout->readLine();
  894. QCOMPARE(passphrase.split(" ").size(), 10);
  895. // Testing with invalid word count
  896. execCmd(dicewareCmd, {"diceware", "-W", "-10"});
  897. QCOMPARE(m_stderr->readLine(), QByteArray("Invalid word count -10\n"));
  898. // Testing with invalid word count format
  899. execCmd(dicewareCmd, {"diceware", "-W", "bleuh"});
  900. QCOMPARE(m_stderr->readLine(), QByteArray("Invalid word count bleuh\n"));
  901. TemporaryFile wordFile;
  902. wordFile.open();
  903. for (int i = 0; i < 4500; ++i) {
  904. wordFile.write(QString("word" + QString::number(i) + "\n").toLatin1());
  905. }
  906. wordFile.close();
  907. execCmd(dicewareCmd, {"diceware", "-W", "11", "-w", wordFile.fileName()});
  908. passphrase = m_stdout->readLine();
  909. const auto words = passphrase.split(" ");
  910. QCOMPARE(words.size(), 11);
  911. QRegularExpression regex("^word\\d+$");
  912. for (const auto& word : words) {
  913. QVERIFY2(regex.match(word).hasMatch(), qPrintable("Word " + word + " was not on the word list"));
  914. }
  915. TemporaryFile smallWordFile;
  916. smallWordFile.open();
  917. for (int i = 0; i < 50; ++i) {
  918. smallWordFile.write(QString("word" + QString::number(i) + "\n").toLatin1());
  919. }
  920. smallWordFile.close();
  921. execCmd(dicewareCmd, {"diceware", "-W", "11", "-w", smallWordFile.fileName()});
  922. QCOMPARE(m_stderr->readLine(), QByteArray("Cannot generate valid passphrases because the wordlist is too short\n"));
  923. }
  924. void TestCli::testEdit()
  925. {
  926. Edit editCmd;
  927. QVERIFY(!editCmd.name.isEmpty());
  928. QVERIFY(editCmd.getDescriptionLine().contains(editCmd.name));
  929. setInput("a");
  930. execCmd(editCmd,
  931. {"edit",
  932. "-u",
  933. "newuser",
  934. "--url",
  935. "https://otherurl.example.com/",
  936. "--notes",
  937. "newnotes",
  938. "-t",
  939. "newtitle",
  940. m_dbFile->fileName(),
  941. "/Sample Entry"});
  942. QCOMPARE(m_stdout->readLine(), QByteArray("Successfully edited entry newtitle.\n"));
  943. auto db = readDatabase();
  944. auto* entry = db->rootGroup()->findEntryByPath("/newtitle");
  945. QVERIFY(entry);
  946. QCOMPARE(entry->username(), QString("newuser"));
  947. QCOMPARE(entry->url(), QString("https://otherurl.example.com/"));
  948. QCOMPARE(entry->password(), QString("Password"));
  949. QCOMPARE(entry->notes(), QString("newnotes"));
  950. // Quiet option
  951. setInput("a");
  952. execCmd(editCmd, {"edit", m_dbFile->fileName(), "-q", "-t", "newertitle", "/newtitle"});
  953. QCOMPARE(m_stderr->readAll(), QByteArray());
  954. QCOMPARE(m_stdout->readAll(), QByteArray());
  955. setInput("a");
  956. execCmd(editCmd, {"edit", "-g", m_dbFile->fileName(), "/newertitle"});
  957. db = readDatabase();
  958. entry = db->rootGroup()->findEntryByPath("/newertitle");
  959. QVERIFY(entry);
  960. QCOMPARE(entry->username(), QString("newuser"));
  961. QCOMPARE(entry->url(), QString("https://otherurl.example.com/"));
  962. QVERIFY(!entry->password().isEmpty());
  963. QVERIFY(entry->password() != QString("Password"));
  964. setInput("a");
  965. execCmd(editCmd, {"edit", "-g", "-L", "34", "-t", "evennewertitle", m_dbFile->fileName(), "/newertitle"});
  966. db = readDatabase();
  967. entry = db->rootGroup()->findEntryByPath("/evennewertitle");
  968. QVERIFY(entry);
  969. QCOMPARE(entry->username(), QString("newuser"));
  970. QCOMPARE(entry->url(), QString("https://otherurl.example.com/"));
  971. QVERIFY(entry->password() != QString("Password"));
  972. QCOMPARE(entry->password().size(), 34);
  973. QRegularExpression defaultPasswordClassesRegex("^[a-zA-Z0-9]+$");
  974. QVERIFY(defaultPasswordClassesRegex.match(entry->password()).hasMatch());
  975. setInput("a");
  976. execCmd(editCmd,
  977. {"edit",
  978. "-g",
  979. "-L",
  980. "20",
  981. "--every-group",
  982. "-s",
  983. "-n",
  984. "--upper",
  985. "-l",
  986. m_dbFile->fileName(),
  987. "/evennewertitle"});
  988. QCOMPARE(m_stdout->readAll(), QByteArray("Successfully edited entry evennewertitle.\n"));
  989. db = readDatabase();
  990. entry = db->rootGroup()->findEntryByPath("/evennewertitle");
  991. QVERIFY(entry);
  992. QCOMPARE(entry->password().size(), 20);
  993. QVERIFY(!defaultPasswordClassesRegex.match(entry->password()).hasMatch());
  994. setInput({"a", "newpassword"});
  995. execCmd(editCmd, {"edit", "-p", m_dbFile->fileName(), "/evennewertitle"});
  996. db = readDatabase();
  997. QVERIFY(db);
  998. entry = db->rootGroup()->findEntryByPath("/evennewertitle");
  999. QVERIFY(entry);
  1000. QCOMPARE(entry->password(), QString("newpassword"));
  1001. // with line break in notes
  1002. setInput("a");
  1003. execCmd(editCmd, {"edit", m_dbFile->fileName(), "--notes", "testing\\nline breaks", "/evennewertitle"});
  1004. db = readDatabase();
  1005. entry = db->rootGroup()->findEntryByPath("/evennewertitle");
  1006. QVERIFY(entry);
  1007. QCOMPARE(entry->notes(), QString("testing\nline breaks"));
  1008. }
  1009. void TestCli::testEstimate_data()
  1010. {
  1011. // clang-format off
  1012. QTest::addColumn<QString>("input");
  1013. QTest::addColumn<QStringList>("searchStrings");
  1014. QTest::newRow("Dictionary")
  1015. << "password"
  1016. << QStringList{"Type: Dictionary", "\tpassword"};
  1017. QTest::newRow("Spatial")
  1018. << "sdfg"
  1019. << QStringList{"Type: Spatial", "\tsdfg"};
  1020. QTest::newRow("Spatial(Rep)")
  1021. << "sdfgsdfg"
  1022. << QStringList{"Type: Spatial(Rep)", "\tsdfgsdfg"};
  1023. QTest::newRow("Dictionary / Sequence")
  1024. << "password123"
  1025. << QStringList{"Type: Dictionary", "Type: Sequence", "\tpassword", "\t123"};
  1026. QTest::newRow("Dict+Leet")
  1027. << "p455w0rd"
  1028. << QStringList{"Type: Dict+Leet", "\tp455w0rd"};
  1029. QTest::newRow("Dictionary(Rep)")
  1030. << "hellohello"
  1031. << QStringList{"Type: Dictionary(Rep)", "\thellohello"};
  1032. QTest::newRow("Sequence(Rep) / Dictionary")
  1033. << "456456foobar"
  1034. << QStringList{"Type: Sequence(Rep)", "Type: Dictionary", "\t456456", "\tfoobar"};
  1035. QTest::newRow("Bruteforce(Rep) / Bruteforce")
  1036. << "xzxzy"
  1037. << QStringList{"Type: Bruteforce(Rep)", "Type: Bruteforce", "\txzxz", "\ty"};
  1038. QTest::newRow("Dictionary / Date(Rep)")
  1039. << "pass20182018"
  1040. << QStringList{"Type: Dictionary", "Type: Date(Rep)", "\tpass", "\t20182018"};
  1041. QTest::newRow("Dictionary / Date / Bruteforce")
  1042. << "mypass2018-2"
  1043. << QStringList{"Type: Dictionary", "Type: Date", "Type: Bruteforce", "\tmypass", "\t2018", "\t-2"};
  1044. QTest::newRow("Strong Password")
  1045. << "E*!%.Qw{t.X,&bafw)\"Q!ah$%;U/"
  1046. << QStringList{"Type: Bruteforce", "\tE*"};
  1047. // TODO: detect passphrases and adjust entropy calculation accordingly (issue #2347)
  1048. QTest::newRow("Strong Passphrase")
  1049. << "squint wooing resupply dangle isolation axis headsman"
  1050. << QStringList{"Type: Dictionary", "Type: Bruteforce", "Multi-word extra bits 22.0", "\tsquint", "\t ", "\twooing"};
  1051. // clang-format on
  1052. }
  1053. void TestCli::testEstimate()
  1054. {
  1055. QFETCH(QString, input);
  1056. QFETCH(QStringList, searchStrings);
  1057. // Calculate expected values since zxcvbn output can vary by platform if different wordlists are used
  1058. const auto e = ZxcvbnMatch(input.toUtf8(), nullptr, nullptr);
  1059. auto length = QString::number(input.length());
  1060. auto entropy = QString("%1").arg(e, 0, 'f', 3);
  1061. auto log10 = QString("%1").arg(e * 0.301029996, 0, 'f', 3);
  1062. Estimate estimateCmd;
  1063. QVERIFY(!estimateCmd.name.isEmpty());
  1064. QVERIFY(estimateCmd.getDescriptionLine().contains(estimateCmd.name));
  1065. setInput(input);
  1066. execCmd(estimateCmd, {"estimate", "-a"});
  1067. auto result = QString(m_stdout->readAll());
  1068. QVERIFY(result.contains("Length " + length));
  1069. QVERIFY(result.contains("Entropy " + entropy));
  1070. QVERIFY(result.contains("Log10 " + log10));
  1071. for (const auto& string : asConst(searchStrings)) {
  1072. QVERIFY2(result.contains(string), qPrintable("String " + string + " missing"));
  1073. }
  1074. }
  1075. void TestCli::testExport()
  1076. {
  1077. Export exportCmd;
  1078. QVERIFY(!exportCmd.name.isEmpty());
  1079. QVERIFY(exportCmd.getDescriptionLine().contains(exportCmd.name));
  1080. setInput("a");
  1081. execCmd(exportCmd, {"export", m_dbFile->fileName()});
  1082. TemporaryFile xmlOutput;
  1083. xmlOutput.open(QIODevice::WriteOnly);
  1084. xmlOutput.write(m_stdout->readAll());
  1085. xmlOutput.close();
  1086. QScopedPointer<Database> db(new Database());
  1087. QVERIFY(db->import(xmlOutput.fileName()));
  1088. auto* entry = db->rootGroup()->findEntryByPath("/Sample Entry");
  1089. QVERIFY(entry);
  1090. QCOMPARE(entry->password(), QString("Password"));
  1091. // Quiet option
  1092. QScopedPointer<Database> dbQuiet(new Database());
  1093. setInput("a");
  1094. execCmd(exportCmd, {"export", "-f", "xml", "-q", m_dbFile->fileName()});
  1095. QCOMPARE(m_stderr->readAll(), QByteArray());
  1096. xmlOutput.open(QIODevice::WriteOnly);
  1097. xmlOutput.write(m_stdout->readAll());
  1098. xmlOutput.close();
  1099. QVERIFY(db->import(xmlOutput.fileName()));
  1100. // CSV exporting
  1101. setInput("a");
  1102. execCmd(exportCmd, {"export", "-f", "csv", m_dbFile->fileName()});
  1103. QByteArray csvHeader = m_stdout->readLine();
  1104. QVERIFY(csvHeader.contains(QByteArray("\"Group\",\"Title\",\"Username\",\"Password\",\"URL\",\"Notes\"")));
  1105. QByteArray csvData = m_stdout->readAll();
  1106. QVERIFY(csvData.contains(QByteArray(
  1107. "\"NewDatabase\",\"Sample Entry\",\"User Name\",\"Password\",\"http://www.somesite.com/\",\"Notes\"")));
  1108. // HTML exporting
  1109. setInput("a");
  1110. execCmd(exportCmd, {"export", "-f", "html", m_dbFile->fileName()});
  1111. QByteArray htmlHeader = m_stdout->readLine();
  1112. QVERIFY(htmlHeader.contains(QByteArray("<meta charset=\"UTF-8\"><title></title>")));
  1113. QByteArray htmlBody = m_stdout->readAll();
  1114. QVERIFY(htmlBody.contains(QByteArray("<h2>NewDatabase</h2>")));
  1115. QVERIFY(htmlBody.contains(QByteArray("<caption>Sample Entry</caption>"
  1116. "<tr><th>User name</th><td class=\"username\">User Name</td></tr>"
  1117. "<tr><th>Password</th><td class=\"password\">Password</td></tr>"
  1118. "<tr><th>URL</th><td class=\"url\"><a "
  1119. "href=\"http://www.somesite.com/\">http://www.somesite.com/</a></td></tr>")));
  1120. // test invalid format
  1121. setInput("a");
  1122. execCmd(exportCmd, {"export", "-f", "yaml", m_dbFile->fileName()});
  1123. m_stderr->readLine(); // Skip password prompt
  1124. QCOMPARE(m_stderr->readLine(), QByteArray("Unsupported format yaml\n"));
  1125. }
  1126. void TestCli::testGenerate_data()
  1127. {
  1128. QTest::addColumn<QStringList>("parameters");
  1129. QTest::addColumn<QString>("pattern");
  1130. QTest::newRow("default") << QStringList{"generate"} << "^[^\r\n]+$";
  1131. QTest::newRow("length") << QStringList{"generate", "-L", "13"} << "^.{13}$";
  1132. QTest::newRow("lowercase") << QStringList{"generate", "-L", "14", "-l"} << "^[a-z]{14}$";
  1133. QTest::newRow("uppercase") << QStringList{"generate", "-L", "15", "--upper"} << "^[A-Z]{15}$";
  1134. QTest::newRow("numbers") << QStringList{"generate", "-L", "16", "-n"} << "^[0-9]{16}$";
  1135. QTest::newRow("special") << QStringList{"generate", "-L", "200", "-s"}
  1136. << R"(^[\(\)\[\]\{\}\.\-*|\\,:;"'\/\_!+-<=>?#$%&^`@~]{200}$)";
  1137. QTest::newRow("special (exclude)") << QStringList{"generate", "-L", "200", "-s", "-x", "+.?@&"}
  1138. << R"(^[\(\)\[\]\{\}\.\-*|\\,:;"'\/\_!-<=>#$%^`~]{200}$)";
  1139. QTest::newRow("extended") << QStringList{"generate", "-L", "50", "-e"}
  1140. << R"(^[^a-zA-Z0-9\(\)\[\]\{\}\.\-\*\|\\,:;"'\/\_!+-<=>?#$%&^`@~]{50}$)";
  1141. QTest::newRow("numbers + lowercase + uppercase")
  1142. << QStringList{"generate", "-L", "16", "-n", "--upper", "-l"} << "^[0-9a-zA-Z]{16}$";
  1143. QTest::newRow("numbers + lowercase + uppercase (exclude)")
  1144. << QStringList{"generate", "-L", "500", "-n", "-U", "-l", "-x", "abcdefg0123@"} << "^[^abcdefg0123@]{500}$";
  1145. QTest::newRow("numbers + lowercase + uppercase (exclude similar)")
  1146. << QStringList{"generate", "-L", "200", "-n", "-U", "-l", "--exclude-similar"} << "^[^l1IO0]{200}$";
  1147. QTest::newRow("uppercase + lowercase (every)")
  1148. << QStringList{"generate", "-L", "2", "--upper", "-l", "--every-group"} << "^[a-z][A-Z]|[A-Z][a-z]$";
  1149. QTest::newRow("numbers + lowercase (every)")
  1150. << QStringList{"generate", "-L", "2", "-n", "-l", "--every-group"} << "^[a-z][0-9]|[0-9][a-z]$";
  1151. QTest::newRow("custom character set")
  1152. << QStringList{"generate", "-L", "200", "-n", "-c", "abc"} << "^[abc0-9]{200}$";
  1153. QTest::newRow("custom character set without extra options uses only custom chars")
  1154. << QStringList{"generate", "-L", "200", "-c", "a"} << "^a{200}$";
  1155. }
  1156. void TestCli::testGenerate()
  1157. {
  1158. QFETCH(QStringList, parameters);
  1159. QFETCH(QString, pattern);
  1160. Generate generateCmd;
  1161. QVERIFY(!generateCmd.name.isEmpty());
  1162. QVERIFY(generateCmd.getDescriptionLine().contains(generateCmd.name));
  1163. for (int i = 0; i < 10; ++i) {
  1164. execCmd(generateCmd, parameters);
  1165. QRegularExpression regex(pattern);
  1166. #ifdef Q_OS_UNIX
  1167. QString password = QString::fromUtf8(m_stdout->readLine());
  1168. #else
  1169. QString password = QString::fromLatin1(m_stdout->readLine());
  1170. #endif
  1171. QVERIFY2(regex.match(password).hasMatch(),
  1172. qPrintable("Password " + password + " does not match pattern " + pattern));
  1173. QCOMPARE(m_stderr->readAll(), QByteArray());
  1174. }
  1175. // Testing with invalid password length
  1176. execCmd(generateCmd, {"generate", "-L", "-10"});
  1177. QCOMPARE(m_stderr->readLine(), QByteArray("Invalid password length -10\n"));
  1178. execCmd(generateCmd, {"generate", "-L", "0"});
  1179. QCOMPARE(m_stderr->readLine(), QByteArray("Invalid password length 0\n"));
  1180. // Testing with invalid word count format
  1181. execCmd(generateCmd, {"generate", "-L", "bleuh"});
  1182. QCOMPARE(m_stderr->readLine(), QByteArray("Invalid password length bleuh\n"));
  1183. }
  1184. void TestCli::testImport()
  1185. {
  1186. Import importCmd;
  1187. QVERIFY(!importCmd.name.isEmpty());
  1188. QVERIFY(importCmd.getDescriptionLine().contains(importCmd.name));
  1189. QScopedPointer<QTemporaryDir> testDir(new QTemporaryDir());
  1190. QString databaseFilename = testDir->path() + "/testImport1.kdbx";
  1191. setInput({"a", "a"});
  1192. execCmd(importCmd, {"import", m_xmlFile->fileName(), databaseFilename, "-p"});
  1193. QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
  1194. QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
  1195. QCOMPARE(m_stdout->readLine(), QByteArray("Successfully imported database.\n"));
  1196. auto db = readDatabase(databaseFilename, "a");
  1197. QVERIFY(db);
  1198. auto* entry = db->rootGroup()->findEntryByPath("/Sample Entry 1");
  1199. QVERIFY(entry);
  1200. QCOMPARE(entry->username(), QString("User Name"));
  1201. // Should refuse to create the database if it already exists.
  1202. execCmd(importCmd, {"import", m_xmlFile->fileName(), databaseFilename});
  1203. // Output should be empty when there is an error.
  1204. QCOMPARE(m_stdout->readAll(), QByteArray());
  1205. QString errorMessage = QString("File " + databaseFilename + " already exists.\n");
  1206. QCOMPARE(m_stderr->readAll(), errorMessage.toUtf8());
  1207. // Testing import with non-existing keyfile
  1208. databaseFilename = testDir->path() + "/testImport2.kdbx";
  1209. QString keyfilePath = testDir->path() + "/keyfile.txt";
  1210. setInput({"a", "a"});
  1211. execCmd(importCmd, {"import", "-p", "-k", keyfilePath, m_xmlFile->fileName(), databaseFilename});
  1212. QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
  1213. QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
  1214. QCOMPARE(m_stdout->readLine(), QByteArray("Successfully imported database.\n"));
  1215. db = readDatabase(databaseFilename, "a", keyfilePath);
  1216. QVERIFY(db);
  1217. // Testing import with existing keyfile
  1218. databaseFilename = testDir->path() + "/testImport3.kdbx";
  1219. setInput({"a", "a"});
  1220. execCmd(importCmd, {"import", "-p", "-k", keyfilePath, m_xmlFile->fileName(), databaseFilename});
  1221. QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
  1222. QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
  1223. QCOMPARE(m_stdout->readLine(), QByteArray("Successfully imported database.\n"));
  1224. db = readDatabase(databaseFilename, "a", keyfilePath);
  1225. QVERIFY(db);
  1226. // Invalid decryption time (format).
  1227. databaseFilename = testDir->path() + "/testCreate_time.kdbx";
  1228. execCmd(importCmd, {"import", "-p", "-t", "NAN", m_xmlFile->fileName(), databaseFilename});
  1229. QCOMPARE(m_stdout->readAll(), QByteArray());
  1230. QCOMPARE(m_stderr->readAll(), QByteArray("Invalid decryption time NAN.\n"));
  1231. // Invalid decryption time (range).
  1232. execCmd(importCmd, {"import", "-p", "-t", "10", m_xmlFile->fileName(), databaseFilename});
  1233. QCOMPARE(m_stdout->readAll(), QByteArray());
  1234. QVERIFY(m_stderr->readAll().contains(QByteArray("Target decryption time must be between")));
  1235. int encryptionTime = 500;
  1236. // Custom encryption time
  1237. setInput({"a", "a"});
  1238. int epochBefore = QDateTime::currentMSecsSinceEpoch();
  1239. execCmd(importCmd,
  1240. {"import", "-p", "-t", QString::number(encryptionTime), m_xmlFile->fileName(), databaseFilename});
  1241. // Removing 100ms to make sure we account for changes in computation time.
  1242. QVERIFY(QDateTime::currentMSecsSinceEpoch() > (epochBefore + encryptionTime - 100));
  1243. QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
  1244. QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
  1245. QCOMPARE(m_stdout->readLine(), QByteArray("Benchmarking key derivation function for 500ms delay.\n"));
  1246. QVERIFY(m_stdout->readLine().contains(QByteArray("rounds for key derivation function.\n")));
  1247. db = readDatabase(databaseFilename, "a");
  1248. QVERIFY(db);
  1249. // Quiet option
  1250. QScopedPointer<QTemporaryDir> testDirQuiet(new QTemporaryDir());
  1251. QString databaseFilenameQuiet = testDirQuiet->path() + "/testImport2.kdbx";
  1252. setInput({"a", "a"});
  1253. execCmd(importCmd, {"import", "-p", "-q", m_xmlFile->fileName(), databaseFilenameQuiet});
  1254. QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
  1255. QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
  1256. QCOMPARE(m_stdout->readLine(), QByteArray());
  1257. db = readDatabase(databaseFilenameQuiet, "a");
  1258. QVERIFY(db);
  1259. }
  1260. void TestCli::testKeyFileOption()
  1261. {
  1262. List listCmd;
  1263. QString keyFilePath(QString(KEEPASSX_TEST_DATA_DIR).append("/KeyFileProtected.key"));
  1264. setInput("a");
  1265. execCmd(listCmd, {"ls", "-k", keyFilePath, m_keyFileProtectedDbFile->fileName()});
  1266. m_stderr->readLine(); // Skip password prompt
  1267. QCOMPARE(m_stderr->readAll(), QByteArray());
  1268. QCOMPARE(m_stdout->readAll(),
  1269. QByteArray("entry1\n"
  1270. "entry2\n"));
  1271. // Should raise an error with no key file.
  1272. setInput("a");
  1273. execCmd(listCmd, {"ls", m_keyFileProtectedDbFile->fileName()});
  1274. QCOMPARE(m_stdout->readAll(), QByteArray());
  1275. QVERIFY(m_stderr->readAll().contains("Invalid credentials were provided"));
  1276. // Should raise an error if key file path is invalid.
  1277. setInput("a");
  1278. execCmd(listCmd, {"ls", "-k", "invalidpath", m_keyFileProtectedDbFile->fileName()});
  1279. m_stderr->readLine(); // skip password prompt
  1280. QCOMPARE(m_stdout->readAll(), QByteArray());
  1281. QCOMPARE(m_stderr->readAll().split(':').at(0), QByteArray("Failed to load key file invalidpath"));
  1282. }
  1283. void TestCli::testNoPasswordOption()
  1284. {
  1285. List listCmd;
  1286. QString keyFilePath(QString(KEEPASSX_TEST_DATA_DIR).append("/KeyFileProtectedNoPassword.key"));
  1287. execCmd(listCmd, {"ls", "-k", keyFilePath, "--no-password", m_keyFileProtectedNoPasswordDbFile->fileName()});
  1288. // Expecting no password prompt
  1289. QCOMPARE(m_stderr->readAll(), QByteArray());
  1290. QCOMPARE(m_stdout->readAll(),
  1291. QByteArray("entry1\n"
  1292. "entry2\n"));
  1293. // Should raise an error with no key file.
  1294. execCmd(listCmd, {"ls", "--no-password", m_keyFileProtectedNoPasswordDbFile->fileName()});
  1295. QCOMPARE(m_stdout->readAll(), QByteArray());
  1296. QVERIFY(m_stderr->readAll().contains("Invalid credentials were provided"));
  1297. }
  1298. void TestCli::testList()
  1299. {
  1300. List listCmd;
  1301. QVERIFY(!listCmd.name.isEmpty());
  1302. QVERIFY(listCmd.getDescriptionLine().contains(listCmd.name));
  1303. setInput("a");
  1304. execCmd(listCmd, {"ls", m_dbFile->fileName()});
  1305. m_stderr->readLine(); // Skip password prompt
  1306. QCOMPARE(m_stderr->readAll(), QByteArray());
  1307. QCOMPARE(m_stdout->readAll(),
  1308. QByteArray("Sample Entry\n"
  1309. "General/\n"
  1310. "Windows/\n"
  1311. "Network/\n"
  1312. "Internet/\n"
  1313. "eMail/\n"
  1314. "Homebanking/\n"));
  1315. // Quiet option
  1316. setInput("a");
  1317. execCmd(listCmd, {"ls", "-q", m_dbFile->fileName()});
  1318. QCOMPARE(m_stderr->readAll(), QByteArray());
  1319. QCOMPARE(m_stdout->readAll(),
  1320. QByteArray("Sample Entry\n"
  1321. "General/\n"
  1322. "Windows/\n"
  1323. "Network/\n"
  1324. "Internet/\n"
  1325. "eMail/\n"
  1326. "Homebanking/\n"));
  1327. setInput("a");
  1328. execCmd(listCmd, {"ls", "-R", m_dbFile->fileName()});
  1329. QCOMPARE(m_stdout->readAll(),
  1330. QByteArray("Sample Entry\n"
  1331. "General/\n"
  1332. " [empty]\n"
  1333. "Windows/\n"
  1334. " [empty]\n"
  1335. "Network/\n"
  1336. " [empty]\n"
  1337. "Internet/\n"
  1338. " [empty]\n"
  1339. "eMail/\n"
  1340. " [empty]\n"
  1341. "Homebanking/\n"
  1342. " Subgroup/\n"
  1343. " Subgroup Entry\n"));
  1344. setInput("a");
  1345. execCmd(listCmd, {"ls", "-R", "-f", m_dbFile->fileName()});
  1346. QCOMPARE(m_stdout->readAll(),
  1347. QByteArray("Sample Entry\n"
  1348. "General/\n"
  1349. "General/[empty]\n"
  1350. "Windows/\n"
  1351. "Windows/[empty]\n"
  1352. "Network/\n"
  1353. "Network/[empty]\n"
  1354. "Internet/\n"
  1355. "Internet/[empty]\n"
  1356. "eMail/\n"
  1357. "eMail/[empty]\n"
  1358. "Homebanking/\n"
  1359. "Homebanking/Subgroup/\n"
  1360. "Homebanking/Subgroup/Subgroup Entry\n"));
  1361. setInput("a");
  1362. execCmd(listCmd, {"ls", "-R", "-f", m_dbFile->fileName(), "/Homebanking"});
  1363. QCOMPARE(m_stdout->readAll(),
  1364. QByteArray("Subgroup/\n"
  1365. "Subgroup/Subgroup Entry\n"));
  1366. setInput("a");
  1367. execCmd(listCmd, {"ls", m_dbFile->fileName(), "/General/"});
  1368. QCOMPARE(m_stdout->readAll(), QByteArray("[empty]\n"));
  1369. setInput("a");
  1370. execCmd(listCmd, {"ls", m_dbFile->fileName(), "/DoesNotExist/"});
  1371. m_stderr->readLine(); // skip password prompt
  1372. QCOMPARE(m_stderr->readAll(), QByteArray("Cannot find group /DoesNotExist/.\n"));
  1373. QCOMPARE(m_stdout->readAll(), QByteArray());
  1374. }
  1375. void TestCli::testMerge()
  1376. {
  1377. Merge mergeCmd;
  1378. QVERIFY(!mergeCmd.name.isEmpty());
  1379. QVERIFY(mergeCmd.getDescriptionLine().contains(mergeCmd.name));
  1380. // load test database and save copies
  1381. auto db = readDatabase();
  1382. QVERIFY(db);
  1383. TemporaryFile targetFile1;
  1384. targetFile1.open();
  1385. targetFile1.close();
  1386. TemporaryFile targetFile2;
  1387. targetFile2.open();
  1388. targetFile2.close();
  1389. TemporaryFile targetFile3;
  1390. targetFile3.open();
  1391. targetFile3.close();
  1392. db->saveAs(targetFile1.fileName());
  1393. db->saveAs(targetFile2.fileName());
  1394. // save another copy with a different password
  1395. auto oldKey = db->key();
  1396. auto key = QSharedPointer<CompositeKey>::create();
  1397. key->addKey(QSharedPointer<PasswordKey>::create("b"));
  1398. db->setKey(key);
  1399. db->saveAs(targetFile3.fileName());
  1400. // Restore the original password
  1401. db->setKey(oldKey);
  1402. // then add a new entry to the in-memory database and save another copy
  1403. auto* entry = new Entry();
  1404. entry->setUuid(QUuid::createUuid());
  1405. entry->setTitle("Some Website");
  1406. entry->setPassword("secretsecretsecret");
  1407. auto* group = db->rootGroup()->findGroupByPath("/Internet/");
  1408. QVERIFY(group);
  1409. group->addEntry(entry);
  1410. TemporaryFile sourceFile;
  1411. sourceFile.open();
  1412. sourceFile.close();
  1413. db->saveAs(sourceFile.fileName());
  1414. setInput("a");
  1415. execCmd(mergeCmd, {"merge", "-s", targetFile1.fileName(), sourceFile.fileName()});
  1416. m_stderr->readLine(); // Skip password prompt
  1417. QCOMPARE(m_stderr->readAll(), QByteArray());
  1418. QList<QByteArray> outLines1 = m_stdout->readAll().split('\n');
  1419. QVERIFY(outLines1.at(0).contains("Overwriting Internet"));
  1420. QVERIFY(outLines1.at(1).contains("Creating missing Some Website"));
  1421. QCOMPARE(outLines1.at(2),
  1422. QString("Successfully merged %1 into %2.").arg(sourceFile.fileName(), targetFile1.fileName()).toUtf8());
  1423. auto mergedDb = QSharedPointer<Database>::create();
  1424. QVERIFY(mergedDb->open(targetFile1.fileName(), oldKey));
  1425. auto* entry1 = mergedDb->rootGroup()->findEntryByPath("/Internet/Some Website");
  1426. QVERIFY(entry1);
  1427. QCOMPARE(entry1->title(), QString("Some Website"));
  1428. QCOMPARE(entry1->password(), QString("secretsecretsecret"));
  1429. // the dry run option should not modify the target database.
  1430. setInput("a");
  1431. execCmd(mergeCmd, {"merge", "--dry-run", "-s", targetFile2.fileName(), sourceFile.fileName()});
  1432. QList<QByteArray> outLines2 = m_stdout->readAll().split('\n');
  1433. QVERIFY(outLines2.at(0).contains("Overwriting Internet"));
  1434. QVERIFY(outLines2.at(1).contains("Creating missing Some Website"));
  1435. QCOMPARE(outLines2.at(2), QByteArray("Database was not modified by merge operation."));
  1436. mergedDb = QSharedPointer<Database>::create();
  1437. QVERIFY(mergedDb->open(targetFile2.fileName(), oldKey));
  1438. entry1 = mergedDb->rootGroup()->findEntryByPath("/Internet/Some Website");
  1439. QVERIFY(!entry1);
  1440. // the dry run option can be used with the quiet option
  1441. setInput("a");
  1442. execCmd(mergeCmd, {"merge", "--dry-run", "-s", "-q", targetFile2.fileName(), sourceFile.fileName()});
  1443. QCOMPARE(m_stderr->readAll(), QByteArray());
  1444. QCOMPARE(m_stdout->readAll(), QByteArray());
  1445. mergedDb = QSharedPointer<Database>::create();
  1446. QVERIFY(mergedDb->open(targetFile2.fileName(), oldKey));
  1447. entry1 = mergedDb->rootGroup()->findEntryByPath("/Internet/Some Website");
  1448. QVERIFY(!entry1);
  1449. // try again with different passwords for both files
  1450. setInput({"b", "a"});
  1451. execCmd(mergeCmd, {"merge", targetFile3.fileName(), sourceFile.fileName()});
  1452. QList<QByteArray> outLines3 = m_stdout->readAll().split('\n');
  1453. QCOMPARE(outLines3.at(2),
  1454. QString("Successfully merged %1 into %2.").arg(sourceFile.fileName(), targetFile3.fileName()).toUtf8());
  1455. mergedDb = QSharedPointer<Database>::create();
  1456. QVERIFY(mergedDb->open(targetFile3.fileName(), key));
  1457. entry1 = mergedDb->rootGroup()->findEntryByPath("/Internet/Some Website");
  1458. QVERIFY(entry1);
  1459. QCOMPARE(entry1->title(), QString("Some Website"));
  1460. QCOMPARE(entry1->password(), QString("secretsecretsecret"));
  1461. // making sure that the message is different if the database was not
  1462. // modified by the merge operation.
  1463. setInput("a");
  1464. execCmd(mergeCmd, {"merge", "-s", sourceFile.fileName(), sourceFile.fileName()});
  1465. QCOMPARE(m_stdout->readAll(), QByteArray("Database was not modified by merge operation.\n"));
  1466. // Quiet option
  1467. setInput("a");
  1468. execCmd(mergeCmd, {"merge", "-q", "-s", sourceFile.fileName(), sourceFile.fileName()});
  1469. QCOMPARE(m_stderr->readAll(), QByteArray());
  1470. QCOMPARE(m_stdout->readAll(), QByteArray());
  1471. // Quiet option without the -s option
  1472. setInput({"a", "a"});
  1473. execCmd(mergeCmd, {"merge", "-q", sourceFile.fileName(), sourceFile.fileName()});
  1474. QCOMPARE(m_stderr->readAll(), QByteArray());
  1475. QCOMPARE(m_stdout->readAll(), QByteArray());
  1476. }
  1477. void TestCli::testMergeWithKeys()
  1478. {
  1479. DatabaseCreate createCmd;
  1480. QVERIFY(!createCmd.name.isEmpty());
  1481. QVERIFY(createCmd.getDescriptionLine().contains(createCmd.name));
  1482. Merge mergeCmd;
  1483. QVERIFY(!mergeCmd.name.isEmpty());
  1484. QVERIFY(mergeCmd.getDescriptionLine().contains(mergeCmd.name));
  1485. QScopedPointer<QTemporaryDir> testDir(new QTemporaryDir());
  1486. QString sourceDatabaseFilename = testDir->path() + "/testSourceDatabase.kdbx";
  1487. QString sourceKeyfilePath = testDir->path() + "/testSourceKeyfile.txt";
  1488. QString targetDatabaseFilename = testDir->path() + "/testTargetDatabase.kdbx";
  1489. QString targetKeyfilePath = testDir->path() + "/testTargetKeyfile.txt";
  1490. setInput({"a", "a"});
  1491. execCmd(createCmd, {"db-create", sourceDatabaseFilename, "-p", "-k", sourceKeyfilePath});
  1492. setInput({"b", "b"});
  1493. execCmd(createCmd, {"db-create", targetDatabaseFilename, "-p", "-k", targetKeyfilePath});
  1494. auto sourceDatabase = readDatabase(sourceDatabaseFilename, "a", sourceKeyfilePath);
  1495. QVERIFY(sourceDatabase);
  1496. auto targetDatabase = readDatabase(targetDatabaseFilename, "b", targetKeyfilePath);
  1497. QVERIFY(targetDatabase);
  1498. auto* rootGroup = new Group();
  1499. rootGroup->setName("root");
  1500. rootGroup->setUuid(QUuid::createUuid());
  1501. auto* group = new Group();
  1502. group->setUuid(QUuid::createUuid());
  1503. group->setParent(rootGroup);
  1504. group->setName("Internet");
  1505. auto* entry = new Entry();
  1506. entry->setUuid(QUuid::createUuid());
  1507. entry->setTitle("Some Website");
  1508. entry->setPassword("secretsecretsecret");
  1509. group->addEntry(entry);
  1510. auto oldGroup = sourceDatabase->setRootGroup(rootGroup);
  1511. delete oldGroup;
  1512. auto* otherRootGroup = new Group();
  1513. otherRootGroup->setName("root");
  1514. otherRootGroup->setUuid(QUuid::createUuid());
  1515. auto* otherGroup = new Group();
  1516. otherGroup->setUuid(QUuid::createUuid());
  1517. otherGroup->setParent(otherRootGroup);
  1518. otherGroup->setName("Internet");
  1519. auto* otherEntry = new Entry();
  1520. otherEntry->setUuid(QUuid::createUuid());
  1521. otherEntry->setTitle("Some Website 2");
  1522. otherEntry->setPassword("secretsecretsecret 2");
  1523. otherGroup->addEntry(otherEntry);
  1524. oldGroup = targetDatabase->setRootGroup(otherRootGroup);
  1525. delete oldGroup;
  1526. sourceDatabase->saveAs(sourceDatabaseFilename);
  1527. targetDatabase->saveAs(targetDatabaseFilename);
  1528. setInput({"b", "a"});
  1529. execCmd(mergeCmd,
  1530. {"merge",
  1531. "-k",
  1532. targetKeyfilePath,
  1533. "--key-file-from",
  1534. sourceKeyfilePath,
  1535. targetDatabaseFilename,
  1536. sourceDatabaseFilename});
  1537. QList<QByteArray> lines = m_stdout->readAll().split('\n');
  1538. QVERIFY(lines.contains(
  1539. QString("Successfully merged %1 into %2.").arg(sourceDatabaseFilename, targetDatabaseFilename).toUtf8()));
  1540. }
  1541. void TestCli::testMove()
  1542. {
  1543. Move moveCmd;
  1544. QVERIFY(!moveCmd.name.isEmpty());
  1545. QVERIFY(moveCmd.getDescriptionLine().contains(moveCmd.name));
  1546. setInput("a");
  1547. execCmd(moveCmd, {"mv", m_dbFile->fileName(), "invalid_entry_path", "invalid_group_path"});
  1548. m_stderr->readLine(); // skip password prompt
  1549. QCOMPARE(m_stderr->readLine(), QByteArray("Could not find entry with path invalid_entry_path.\n"));
  1550. QCOMPARE(m_stdout->readLine(), QByteArray());
  1551. setInput("a");
  1552. execCmd(moveCmd, {"mv", m_dbFile->fileName(), "Sample Entry", "invalid_group_path"});
  1553. m_stderr->readLine(); // skip password prompt
  1554. QCOMPARE(m_stderr->readLine(), QByteArray("Could not find group with path invalid_group_path.\n"));
  1555. QCOMPARE(m_stdout->readLine(), QByteArray());
  1556. setInput("a");
  1557. execCmd(moveCmd, {"mv", m_dbFile->fileName(), "Sample Entry", "General/"});
  1558. m_stderr->readLine(); // skip password prompt
  1559. QCOMPARE(m_stderr->readLine(), QByteArray());
  1560. QCOMPARE(m_stdout->readLine(), QByteArray("Successfully moved entry Sample Entry to group General/.\n"));
  1561. auto db = readDatabase();
  1562. auto* entry = db->rootGroup()->findEntryByPath("General/Sample Entry");
  1563. QVERIFY(entry);
  1564. // Test that not modified if the same group is destination.
  1565. setInput("a");
  1566. execCmd(moveCmd, {"mv", m_dbFile->fileName(), "General/Sample Entry", "General/"});
  1567. m_stderr->readLine(); // skip password prompt
  1568. QCOMPARE(m_stderr->readLine(), QByteArray("Entry is already in group General/.\n"));
  1569. QCOMPARE(m_stdout->readLine(), QByteArray());
  1570. // sanity check
  1571. db = readDatabase();
  1572. entry = db->rootGroup()->findEntryByPath("General/Sample Entry");
  1573. QVERIFY(entry);
  1574. }
  1575. void TestCli::testRemove()
  1576. {
  1577. Remove removeCmd;
  1578. QVERIFY(!removeCmd.name.isEmpty());
  1579. QVERIFY(removeCmd.getDescriptionLine().contains(removeCmd.name));
  1580. // load test database and save a copy with disabled recycle bin
  1581. auto db = readDatabase();
  1582. QVERIFY(db);
  1583. TemporaryFile fileCopy;
  1584. fileCopy.open();
  1585. fileCopy.close();
  1586. db->metadata()->setRecycleBinEnabled(false);
  1587. db->saveAs(fileCopy.fileName());
  1588. // delete entry and verify
  1589. setInput("a");
  1590. execCmd(removeCmd, {"rm", m_dbFile->fileName(), "/Sample Entry"});
  1591. m_stderr->readLine(); // skip password prompt
  1592. QCOMPARE(m_stderr->readAll(), QByteArray());
  1593. QCOMPARE(m_stdout->readAll(), QByteArray("Successfully recycled entry Sample Entry.\n"));
  1594. auto readBackDb = readDatabase();
  1595. QVERIFY(readBackDb);
  1596. QVERIFY(!readBackDb->rootGroup()->findEntryByPath("/Sample Entry"));
  1597. QVERIFY(readBackDb->rootGroup()->findEntryByPath(QString("/%1/Sample Entry").arg(Group::tr("Recycle Bin"))));
  1598. // try again, this time without recycle bin
  1599. setInput("a");
  1600. execCmd(removeCmd, {"rm", fileCopy.fileName(), "/Sample Entry"});
  1601. m_stderr->readLine(); // skip password prompt
  1602. QCOMPARE(m_stdout->readAll(), QByteArray("Successfully deleted entry Sample Entry.\n"));
  1603. readBackDb = readDatabase(fileCopy.fileName(), "a");
  1604. QVERIFY(readBackDb);
  1605. QVERIFY(!readBackDb->rootGroup()->findEntryByPath("/Sample Entry"));
  1606. QVERIFY(!readBackDb->rootGroup()->findEntryByPath(QString("/%1/Sample Entry").arg(Group::tr("Recycle Bin"))));
  1607. // finally, try deleting a non-existent entry
  1608. setInput("a");
  1609. execCmd(removeCmd, {"rm", fileCopy.fileName(), "/Sample Entry"});
  1610. m_stderr->readLine(); // skip password prompt
  1611. QCOMPARE(m_stderr->readAll(), QByteArray("Entry /Sample Entry not found.\n"));
  1612. QCOMPARE(m_stdout->readAll(), QByteArray());
  1613. // try deleting a directory, should fail
  1614. setInput("a");
  1615. execCmd(removeCmd, {"rm", fileCopy.fileName(), "/General"});
  1616. m_stderr->readLine(); // skip password prompt
  1617. QCOMPARE(m_stderr->readAll(), QByteArray("Entry /General not found.\n"));
  1618. QCOMPARE(m_stdout->readAll(), QByteArray());
  1619. }
  1620. void TestCli::testRemoveGroup()
  1621. {
  1622. RemoveGroup removeGroupCmd;
  1623. QVERIFY(!removeGroupCmd.name.isEmpty());
  1624. QVERIFY(removeGroupCmd.getDescriptionLine().contains(removeGroupCmd.name));
  1625. // try deleting a directory, should recycle it first.
  1626. setInput("a");
  1627. execCmd(removeGroupCmd, {"rmdir", m_dbFile->fileName(), "/General"});
  1628. m_stderr->readLine(); // skip password prompt
  1629. QCOMPARE(m_stderr->readAll(), QByteArray());
  1630. QCOMPARE(m_stdout->readAll(), QByteArray("Successfully recycled group /General.\n"));
  1631. auto db = readDatabase();
  1632. auto* group = db->rootGroup()->findGroupByPath("General");
  1633. QVERIFY(!group);
  1634. // try deleting a directory again, should delete it permanently.
  1635. setInput("a");
  1636. execCmd(removeGroupCmd, {"rmdir", m_dbFile->fileName(), "Recycle Bin/General"});
  1637. m_stderr->readLine(); // skip password prompt
  1638. QCOMPARE(m_stderr->readAll(), QByteArray());
  1639. QCOMPARE(m_stdout->readAll(), QByteArray("Successfully deleted group Recycle Bin/General.\n"));
  1640. db = readDatabase();
  1641. group = db->rootGroup()->findGroupByPath("Recycle Bin/General");
  1642. QVERIFY(!group);
  1643. // try deleting an invalid group, should fail.
  1644. setInput("a");
  1645. execCmd(removeGroupCmd, {"rmdir", m_dbFile->fileName(), "invalid"});
  1646. m_stderr->readLine(); // skip password prompt
  1647. QCOMPARE(m_stderr->readAll(), QByteArray("Group invalid not found.\n"));
  1648. QCOMPARE(m_stdout->readAll(), QByteArray());
  1649. // Should fail to remove the root group.
  1650. setInput("a");
  1651. execCmd(removeGroupCmd, {"rmdir", m_dbFile->fileName(), "/"});
  1652. m_stderr->readLine(); // skip password prompt
  1653. QCOMPARE(m_stderr->readAll(), QByteArray("Cannot remove root group from database.\n"));
  1654. QCOMPARE(m_stdout->readAll(), QByteArray());
  1655. }
  1656. void TestCli::testRemoveQuiet()
  1657. {
  1658. Remove removeCmd;
  1659. QVERIFY(!removeCmd.name.isEmpty());
  1660. QVERIFY(removeCmd.getDescriptionLine().contains(removeCmd.name));
  1661. // delete entry and verify
  1662. setInput("a");
  1663. execCmd(removeCmd, {"rm", "-q", m_dbFile->fileName(), "/Sample Entry"});
  1664. QCOMPARE(m_stderr->readAll(), QByteArray());
  1665. QCOMPARE(m_stdout->readAll(), QByteArray());
  1666. auto db = readDatabase();
  1667. QVERIFY(db);
  1668. QVERIFY(!db->rootGroup()->findEntryByPath("/Sample Entry"));
  1669. QVERIFY(db->rootGroup()->findEntryByPath(QString("/%1/Sample Entry").arg(Group::tr("Recycle Bin"))));
  1670. // remove the entry completely
  1671. setInput("a");
  1672. execCmd(removeCmd, {"rm", "-q", m_dbFile->fileName(), QString("/%1/Sample Entry").arg(Group::tr("Recycle Bin"))});
  1673. QCOMPARE(m_stderr->readAll(), QByteArray());
  1674. QCOMPARE(m_stdout->readAll(), QByteArray());
  1675. db = readDatabase();
  1676. QVERIFY(!db->rootGroup()->findEntryByPath("/Sample Entry"));
  1677. QVERIFY(!db->rootGroup()->findEntryByPath(QString("/%1/Sample Entry").arg(Group::tr("Recycle Bin"))));
  1678. }
  1679. void TestCli::testSearch()
  1680. {
  1681. Search searchCmd;
  1682. QVERIFY(!searchCmd.name.isEmpty());
  1683. QVERIFY(searchCmd.getDescriptionLine().contains(searchCmd.name));
  1684. setInput("a");
  1685. execCmd(searchCmd, {"search", m_dbFile->fileName(), "Sample"});
  1686. m_stderr->readLine(); // Skip password prompt
  1687. QCOMPARE(m_stderr->readAll(), QByteArray());
  1688. QCOMPARE(m_stdout->readAll(), QByteArray("/Sample Entry\n"));
  1689. // Quiet option
  1690. setInput("a");
  1691. execCmd(searchCmd, {"search", m_dbFile->fileName(), "-q", "Sample"});
  1692. QCOMPARE(m_stderr->readAll(), QByteArray());
  1693. QCOMPARE(m_stdout->readAll(), QByteArray("/Sample Entry\n"));
  1694. setInput("a");
  1695. execCmd(searchCmd, {"search", m_dbFile->fileName(), "Does Not Exist"});
  1696. m_stderr->readLine(); // skip password prompt
  1697. QCOMPARE(m_stderr->readAll(), QByteArray("No results for that search term.\n"));
  1698. QCOMPARE(m_stdout->readAll(), QByteArray());
  1699. // write a modified database
  1700. auto db = readDatabase();
  1701. QVERIFY(db);
  1702. auto* group = db->rootGroup()->findGroupByPath("/General/");
  1703. QVERIFY(group);
  1704. auto* entry = new Entry();
  1705. entry->setUuid(QUuid::createUuid());
  1706. entry->setTitle("New Entry");
  1707. group->addEntry(entry);
  1708. TemporaryFile tmpFile;
  1709. tmpFile.open();
  1710. tmpFile.close();
  1711. db->saveAs(tmpFile.fileName());
  1712. setInput("a");
  1713. execCmd(searchCmd, {"search", tmpFile.fileName(), "title:New"});
  1714. QCOMPARE(m_stdout->readAll(), QByteArray("/General/New Entry\n"));
  1715. setInput("a");
  1716. execCmd(searchCmd, {"search", tmpFile.fileName(), "title:Entry"});
  1717. QCOMPARE(m_stdout->readAll(),
  1718. QByteArray("/Sample Entry\n/General/New Entry\n/Homebanking/Subgroup/Subgroup Entry\n"));
  1719. setInput("a");
  1720. execCmd(searchCmd, {"search", tmpFile.fileName(), "group:General"});
  1721. QCOMPARE(m_stdout->readAll(), QByteArray("/General/New Entry\n"));
  1722. setInput("a");
  1723. execCmd(searchCmd, {"search", tmpFile.fileName(), "group:NewDatabase"});
  1724. QCOMPARE(m_stdout->readAll(), QByteArray("/Sample Entry\n"));
  1725. setInput("a");
  1726. execCmd(searchCmd, {"search", tmpFile.fileName(), "group:/NewDatabase"});
  1727. QCOMPARE(m_stdout->readAll(),
  1728. QByteArray("/Sample Entry\n/General/New Entry\n/Homebanking/Subgroup/Subgroup Entry\n"));
  1729. setInput("a");
  1730. execCmd(searchCmd, {"search", tmpFile.fileName(), "url:bank"});
  1731. QCOMPARE(m_stdout->readAll(), QByteArray("/Homebanking/Subgroup/Subgroup Entry\n"));
  1732. setInput("a");
  1733. execCmd(searchCmd, {"search", tmpFile.fileName(), "u:User Name"});
  1734. QCOMPARE(m_stdout->readAll(), QByteArray("/Sample Entry\n/Homebanking/Subgroup/Subgroup Entry\n"));
  1735. }
  1736. void TestCli::testShow()
  1737. {
  1738. Show showCmd;
  1739. QVERIFY(!showCmd.name.isEmpty());
  1740. QVERIFY(showCmd.getDescriptionLine().contains(showCmd.name));
  1741. setInput("a");
  1742. execCmd(showCmd, {"show", m_dbFile->fileName(), "/Sample Entry"});
  1743. m_stderr->readLine(); // Skip password prompt
  1744. QCOMPARE(m_stderr->readAll(), QByteArray());
  1745. QCOMPARE(m_stdout->readAll(),
  1746. QByteArray("Title: Sample Entry\n"
  1747. "UserName: User Name\n"
  1748. "Password: PROTECTED\n"
  1749. "URL: http://www.somesite.com/\n"
  1750. "Notes: Notes\n"
  1751. "Uuid: {9f4544c2-ab00-c74a-8a1a-6eaf26cf57e9}\n"
  1752. "Tags: \n"));
  1753. setInput("a");
  1754. execCmd(showCmd, {"show", "-s", m_dbFile->fileName(), "/Sample Entry"});
  1755. QCOMPARE(m_stdout->readAll(),
  1756. QByteArray("Title: Sample Entry\n"
  1757. "UserName: User Name\n"
  1758. "Password: Password\n"
  1759. "URL: http://www.somesite.com/\n"
  1760. "Notes: Notes\n"
  1761. "Uuid: {9f4544c2-ab00-c74a-8a1a-6eaf26cf57e9}\n"
  1762. "Tags: \n"));
  1763. setInput("a");
  1764. execCmd(showCmd, {"show", m_dbFile->fileName(), "-q", "/Sample Entry"});
  1765. QCOMPARE(m_stderr->readAll(), QByteArray());
  1766. QCOMPARE(m_stdout->readAll(),
  1767. QByteArray("Title: Sample Entry\n"
  1768. "UserName: User Name\n"
  1769. "Password: PROTECTED\n"
  1770. "URL: http://www.somesite.com/\n"
  1771. "Notes: Notes\n"
  1772. "Uuid: {9f4544c2-ab00-c74a-8a1a-6eaf26cf57e9}\n"
  1773. "Tags: \n"));
  1774. setInput("a");
  1775. execCmd(showCmd, {"show", m_dbFile->fileName(), "--show-attachments", "/Sample Entry"});
  1776. m_stderr->readLine(); // Skip password prompt
  1777. QCOMPARE(m_stderr->readAll(), QByteArray());
  1778. QCOMPARE(m_stdout->readAll(),
  1779. QByteArray("Title: Sample Entry\n"
  1780. "UserName: User Name\n"
  1781. "Password: PROTECTED\n"
  1782. "URL: http://www.somesite.com/\n"
  1783. "Notes: Notes\n"
  1784. "Uuid: {9f4544c2-ab00-c74a-8a1a-6eaf26cf57e9}\n"
  1785. "Tags: \n"
  1786. "\n"
  1787. "Attachments:\n"
  1788. " Sample attachment.txt (15 B)\n"));
  1789. setInput("a");
  1790. execCmd(showCmd, {"show", m_dbFile->fileName(), "--show-attachments", "/Homebanking/Subgroup/Subgroup Entry"});
  1791. m_stderr->readLine(); // Skip password prompt
  1792. QCOMPARE(m_stderr->readAll(), QByteArray());
  1793. QCOMPARE(m_stdout->readAll(),
  1794. QByteArray("Title: Subgroup Entry\n"
  1795. "UserName: Bank User Name\n"
  1796. "Password: PROTECTED\n"
  1797. "URL: https://www.bank.com\n"
  1798. "Notes: Important note\n"
  1799. "Uuid: {20b183fd-6878-4506-a50b-06d30792aa10}\n"
  1800. "Tags: \n"
  1801. "\n"
  1802. "No attachments present.\n"));
  1803. setInput("a");
  1804. execCmd(showCmd, {"show", "-a", "Title", m_dbFile->fileName(), "/Sample Entry"});
  1805. QCOMPARE(m_stdout->readAll(), QByteArray("Sample Entry\n"));
  1806. setInput("a");
  1807. execCmd(showCmd, {"show", "-a", "Password", m_dbFile->fileName(), "/Sample Entry"});
  1808. QCOMPARE(m_stdout->readAll(), QByteArray("Password\n"));
  1809. setInput("a");
  1810. execCmd(showCmd, {"show", "-a", "Uuid", m_dbFile->fileName(), "/Sample Entry"});
  1811. QCOMPARE(m_stdout->readAll(), QByteArray("{9f4544c2-ab00-c74a-8a1a-6eaf26cf57e9}\n"));
  1812. setInput("a");
  1813. execCmd(showCmd, {"show", "-a", "Title", "-a", "URL", m_dbFile->fileName(), "/Sample Entry"});
  1814. QCOMPARE(m_stdout->readAll(),
  1815. QByteArray("Sample Entry\n"
  1816. "http://www.somesite.com/\n"));
  1817. // Test case insensitivity
  1818. setInput("a");
  1819. execCmd(showCmd, {"show", "-a", "TITLE", "-a", "URL", m_dbFile->fileName(), "/Sample Entry"});
  1820. QCOMPARE(m_stdout->readAll(),
  1821. QByteArray("Sample Entry\n"
  1822. "http://www.somesite.com/\n"));
  1823. setInput("a");
  1824. execCmd(showCmd, {"show", "-a", "DoesNotExist", m_dbFile->fileName(), "/Sample Entry"});
  1825. QCOMPARE(m_stdout->readAll(), QByteArray());
  1826. QVERIFY(m_stderr->readAll().contains("ERROR: unknown attribute DoesNotExist.\n"));
  1827. setInput("a");
  1828. execCmd(showCmd, {"show", "-t", m_dbFile->fileName(), "/Sample Entry"});
  1829. QVERIFY(isTotp(m_stdout->readAll()));
  1830. setInput("a");
  1831. execCmd(showCmd, {"show", "-a", "Title", m_dbFile->fileName(), "--totp", "/Sample Entry"});
  1832. QCOMPARE(m_stdout->readLine(), QByteArray("Sample Entry\n"));
  1833. QVERIFY(isTotp(m_stdout->readAll()));
  1834. setInput("a");
  1835. execCmd(showCmd, {"show", m_dbFile2->fileName(), "--totp", "/Sample Entry"});
  1836. QCOMPARE(m_stdout->readAll(), QByteArray());
  1837. QVERIFY(m_stderr->readAll().contains("Entry with path /Sample Entry has no TOTP set up.\n"));
  1838. // Show with ambiguous attributes
  1839. setInput("a");
  1840. execCmd(showCmd, {"show", m_dbFile->fileName(), "-a", "Testattribute1", "/Sample Entry"});
  1841. QCOMPARE(m_stdout->readAll(), QByteArray());
  1842. QVERIFY(m_stderr->readAll().contains("ERROR: attribute Testattribute1 is ambiguous"));
  1843. setInput("a");
  1844. execCmd(showCmd, {"show", "--all", m_dbFile->fileName(), "/Sample Entry"});
  1845. QCOMPARE(m_stdout->readAll(),
  1846. QByteArray("Title: Sample Entry\n"
  1847. "UserName: User Name\n"
  1848. "Password: PROTECTED\n"
  1849. "URL: http://www.somesite.com/\n"
  1850. "Notes: Notes\n"
  1851. "Uuid: {9f4544c2-ab00-c74a-8a1a-6eaf26cf57e9}\n"
  1852. "Tags: \n"
  1853. "TOTP Seed: PROTECTED\n"
  1854. "TOTP Settings: 30;6\n"
  1855. "TestAttribute1: b\n"
  1856. "testattribute1: a\n"));
  1857. }
  1858. void TestCli::testInvalidDbFiles()
  1859. {
  1860. Show showCmd;
  1861. QString nonExistentDbPath("/foo/bar/baz");
  1862. QString directoryName("/");
  1863. execCmd(showCmd, {"show", nonExistentDbPath, "/Sample Entry"});
  1864. QCOMPARE(QString(m_stderr->readAll()),
  1865. QObject::tr("Failed to open database file %1: not found").arg(nonExistentDbPath) + "\n");
  1866. QCOMPARE(m_stdout->readAll(), QByteArray());
  1867. execCmd(showCmd, {"show", directoryName, "whatever"});
  1868. QCOMPARE(QString(m_stderr->readAll()),
  1869. QObject::tr("Failed to open database file %1: not a plain file").arg(directoryName) + "\n");
  1870. // Create a write-only file and try to open it.
  1871. // QFileInfo.isReadable returns 'true' on Windows, even after the call to
  1872. // setPermissions(WriteOwner) and with NTFS permissions enabled, so this
  1873. // check doesn't work.
  1874. #if !defined(Q_OS_WIN)
  1875. QTemporaryFile tempFile;
  1876. QVERIFY(tempFile.open());
  1877. QString path = QFileInfo(tempFile).absoluteFilePath();
  1878. QVERIFY(tempFile.setPermissions(QFileDevice::WriteOwner));
  1879. execCmd(showCmd, {"show", path, "some entry"});
  1880. QCOMPARE(QString(m_stderr->readAll()),
  1881. QObject::tr("Failed to open database file %1: not readable").arg(path) + "\n");
  1882. #endif // Q_OS_WIN
  1883. }
  1884. /**
  1885. * Secret key for the YubiKey slot used by the unit test is
  1886. * 1c e3 0f d7 8d 20 dc fa 40 b5 0c 18 77 9a fb 0f 02 28 8d b7
  1887. * This secret can be on either slot but must be passive.
  1888. */
  1889. void TestCli::testYubiKeyOption()
  1890. {
  1891. if (!YubiKey::instance()->isInitialized()) {
  1892. QSKIP("Unable to initialize YubiKey interface.");
  1893. }
  1894. YubiKey::instance()->findValidKeys();
  1895. const auto keys = YubiKey::instance()->foundKeys().keys();
  1896. if (keys.isEmpty()) {
  1897. QSKIP("No YubiKey devices were detected.");
  1898. }
  1899. bool wouldBlock = false;
  1900. QByteArray challenge("CLITest");
  1901. Botan::secure_vector<char> response;
  1902. QByteArray expected("\xA2\x3B\x94\x00\xBE\x47\x9A\x30\xA9\xEB\x50\x9B\x85\x56\x5B\x6B\x30\x25\xB4\x8E", 20);
  1903. // Find a key that as configured for this test
  1904. YubiKeySlot pKey(0, 0);
  1905. for (auto key : keys) {
  1906. if (YubiKey::instance()->testChallenge(key, &wouldBlock) && !wouldBlock) {
  1907. YubiKey::instance()->challenge(key, challenge, response);
  1908. if (std::memcmp(response.data(), expected.data(), expected.size()) == 0) {
  1909. pKey = key;
  1910. break;
  1911. }
  1912. Tools::wait(100);
  1913. }
  1914. }
  1915. if (pKey.first == 0 && pKey.second == 0) {
  1916. QSKIP("No YubiKey is properly configured to perform this test.");
  1917. }
  1918. List listCmd;
  1919. Add addCmd;
  1920. setInput("a");
  1921. execCmd(listCmd,
  1922. {"ls",
  1923. "-y",
  1924. QString("%1:%2").arg(QString::number(pKey.second), QString::number(pKey.first)),
  1925. m_yubiKeyProtectedDbFile->fileName()});
  1926. m_stderr->readLine(); // skip password prompt
  1927. QCOMPARE(m_stderr->readAll(), QByteArray());
  1928. QCOMPARE(m_stdout->readAll(),
  1929. QByteArray("entry1\n"
  1930. "entry2\n"));
  1931. // Should raise an error with no yubikey slot.
  1932. setInput("a");
  1933. execCmd(listCmd, {"ls", m_yubiKeyProtectedDbFile->fileName()});
  1934. m_stderr->readLine(); // skip password prompt
  1935. QCOMPARE(m_stderr->readLine(),
  1936. QByteArray("Error while reading the database: Invalid credentials were provided, please try again.\n"));
  1937. QCOMPARE(m_stderr->readLine(),
  1938. QByteArray("If this reoccurs, then your database file may be corrupt. (HMAC mismatch)\n"));
  1939. QCOMPARE(m_stdout->readAll(), QByteArray());
  1940. // Should raise an error if yubikey slot is not a string
  1941. setInput("a");
  1942. execCmd(listCmd, {"ls", "-y", "invalidslot", m_yubiKeyProtectedDbFile->fileName()});
  1943. m_stderr->readLine(); // skip password prompt
  1944. QCOMPARE(m_stderr->readAll().split(':').at(0), QByteArray("Invalid YubiKey slot invalidslot\n"));
  1945. QCOMPARE(m_stdout->readAll(), QByteArray());
  1946. // Should raise an error if yubikey slot is invalid.
  1947. setInput("a");
  1948. execCmd(listCmd, {"ls", "-y", "3", m_yubiKeyProtectedDbFile->fileName()});
  1949. m_stderr->readLine(); // skip password prompt
  1950. QCOMPARE(m_stderr->readAll().split(':').at(0), QByteArray("Invalid YubiKey slot 3\n"));
  1951. QCOMPARE(m_stdout->readAll(), QByteArray());
  1952. }
  1953. void TestCli::testNonAscii()
  1954. {
  1955. QProcess process;
  1956. process.setProcessChannelMode(QProcess::MergedChannels);
  1957. process.start(
  1958. KEEPASSX_CLI_PATH,
  1959. QStringList(
  1960. {"show", "-a", "password", m_nonAsciiDbFile->fileName(), QString::fromUtf8("\xe7\xa7\x98\xe5\xaf\x86")}));
  1961. process.waitForStarted();
  1962. QCOMPARE(process.state(), QProcess::ProcessState::Running);
  1963. // Write password.
  1964. process.write("\xce\x94\xc3\xb6\xd8\xb6\n");
  1965. process.closeWriteChannel();
  1966. process.waitForFinished();
  1967. process.readLine(); // skip password prompt
  1968. QByteArray password = process.readLine();
  1969. QCOMPARE(QString::fromUtf8(password).trimmed(),
  1970. QString::fromUtf8("\xf0\x9f\x9a\x97\xf0\x9f\x90\x8e\xf0\x9f\x94\x8b\xf0\x9f\x93\x8e"));
  1971. }
  1972. void TestCli::testCommandParsing_data()
  1973. {
  1974. QTest::addColumn<QString>("input");
  1975. QTest::addColumn<QStringList>("expectedOutput");
  1976. QTest::newRow("basic") << "hello world" << QStringList({"hello", "world"});
  1977. QTest::newRow("basic escaping") << "hello\\ world" << QStringList({"hello world"});
  1978. QTest::newRow("quoted string") << "\"hello world\"" << QStringList({"hello world"});
  1979. QTest::newRow("multiple params") << "show Passwords/Internet" << QStringList({"show", "Passwords/Internet"});
  1980. QTest::newRow("quoted string inside param")
  1981. << R"(ls foo\ bar\ baz"quoted")" << QStringList({"ls", "foo bar baz\"quoted\""});
  1982. QTest::newRow("multiple whitespace") << "hello world" << QStringList({"hello", "world"});
  1983. QTest::newRow("single slash char") << "\\" << QStringList({"\\"});
  1984. QTest::newRow("double backslash entry name") << "show foo\\\\\\\\bar" << QStringList({"show", "foo\\\\bar"});
  1985. }
  1986. void TestCli::testCommandParsing()
  1987. {
  1988. QFETCH(QString, input);
  1989. QFETCH(QStringList, expectedOutput);
  1990. QStringList result = Utils::splitCommandString(input);
  1991. QCOMPARE(result.size(), expectedOutput.size());
  1992. for (int i = 0; i < expectedOutput.size(); ++i) {
  1993. QCOMPARE(result[i], expectedOutput[i]);
  1994. }
  1995. }
  1996. void TestCli::testOpen()
  1997. {
  1998. Open openCmd;
  1999. setInput("a");
  2000. execCmd(openCmd, {"open", m_dbFile->fileName()});
  2001. QVERIFY(openCmd.currentDatabase);
  2002. List listCmd;
  2003. // Set a current database, simulating interactive mode.
  2004. listCmd.currentDatabase = openCmd.currentDatabase;
  2005. execCmd(listCmd, {"ls"});
  2006. QByteArray expectedOutput("Sample Entry\n"
  2007. "General/\n"
  2008. "Windows/\n"
  2009. "Network/\n"
  2010. "Internet/\n"
  2011. "eMail/\n"
  2012. "Homebanking/\n");
  2013. QByteArray actualOutput = m_stdout->readAll();
  2014. actualOutput.truncate(expectedOutput.length());
  2015. QCOMPARE(actualOutput, expectedOutput);
  2016. }
  2017. void TestCli::testHelp()
  2018. {
  2019. Help helpCmd;
  2020. Commands::setupCommands(false);
  2021. execCmd(helpCmd, {"help"});
  2022. QVERIFY(m_stdout->readAll().contains("Available commands"));
  2023. List listCmd;
  2024. execCmd(helpCmd, {"help", "ls"});
  2025. QVERIFY(m_stdout->readAll().contains(listCmd.description.toLatin1()));
  2026. }