2
* Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
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.
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.
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/>.
20
#include "config-keepassx-tests.h"
21
#include "core/Bootstrap.h"
22
#include "core/Config.h"
23
#include "core/Group.h"
24
#include "core/Metadata.h"
25
#include "core/Tools.h"
26
#include "crypto/Crypto.h"
27
#include "keys/FileKey.h"
28
#include "keys/drivers/YubiKey.h"
29
#include "zxcvbn/zxcvbn.h"
32
#include "cli/AddGroup.h"
33
#include "cli/Analyze.h"
34
#include "cli/AttachmentExport.h"
35
#include "cli/AttachmentImport.h"
36
#include "cli/AttachmentRemove.h"
38
#include "cli/DatabaseCreate.h"
39
#include "cli/DatabaseEdit.h"
40
#include "cli/DatabaseInfo.h"
41
#include "cli/Diceware.h"
43
#include "cli/Estimate.h"
44
#include "cli/Export.h"
45
#include "cli/Generate.h"
47
#include "cli/Import.h"
52
#include "cli/Remove.h"
53
#include "cli/RemoveGroup.h"
54
#include "cli/Search.h"
61
#include <QtConcurrent>
65
void TestCli::initTestCase()
67
QVERIFY(Crypto::init());
69
Config::createTempFileInstance();
70
QLocale::setDefault(QLocale::c());
71
Bootstrap::bootstrap();
73
m_devNull.reset(new QFile());
75
m_devNull->open(fopen("nul", "w"), QIODevice::WriteOnly);
77
m_devNull->open(fopen("/dev/null", "w"), QIODevice::WriteOnly);
79
Utils::DEVNULL.setDevice(m_devNull.data());
84
const auto file = QString(KEEPASSX_TEST_DATA_DIR).append("/%1");
86
m_dbFile.reset(new TemporaryFile());
87
m_dbFile->copyFromFile(file.arg("NewDatabase.kdbx"));
89
m_dbFile2.reset(new TemporaryFile());
90
m_dbFile2->copyFromFile(file.arg("NewDatabase2.kdbx"));
92
m_dbFileMulti.reset(new TemporaryFile());
93
m_dbFileMulti->copyFromFile(file.arg("NewDatabaseMulti.kdbx"));
95
m_xmlFile.reset(new TemporaryFile());
96
m_xmlFile->copyFromFile(file.arg("NewDatabase.xml"));
98
m_keyFileProtectedDbFile.reset(new TemporaryFile());
99
m_keyFileProtectedDbFile->copyFromFile(file.arg("KeyFileProtected.kdbx"));
101
m_keyFileProtectedNoPasswordDbFile.reset(new TemporaryFile());
102
m_keyFileProtectedNoPasswordDbFile->copyFromFile(file.arg("KeyFileProtectedNoPassword.kdbx"));
104
m_yubiKeyProtectedDbFile.reset(new TemporaryFile());
105
m_yubiKeyProtectedDbFile->copyFromFile(file.arg("YubiKeyProtectedPasswords.kdbx"));
107
m_nonAsciiDbFile.reset(new TemporaryFile());
108
m_nonAsciiDbFile->copyFromFile(file.arg("NonAscii.kdbx"));
110
m_stdout.reset(new QBuffer());
111
m_stdout->open(QIODevice::ReadWrite);
112
Utils::STDOUT.setDevice(m_stdout.data());
114
m_stderr.reset(new QBuffer());
115
m_stderr->open(QIODevice::ReadWrite);
116
Utils::STDERR.setDevice(m_stderr.data());
118
m_stdin.reset(new QBuffer());
119
m_stdin->open(QIODevice::ReadWrite);
120
Utils::STDIN.setDevice(m_stdin.data());
123
void TestCli::cleanup()
127
m_dbFileMulti.reset();
128
m_keyFileProtectedDbFile.reset();
129
m_keyFileProtectedNoPasswordDbFile.reset();
130
m_yubiKeyProtectedDbFile.reset();
132
Utils::STDOUT.setDevice(nullptr);
133
Utils::STDERR.setDevice(nullptr);
134
Utils::STDIN.setDevice(nullptr);
137
void TestCli::cleanupTestCase()
142
QSharedPointer<Database> TestCli::readDatabase(const QString& filename, const QString& pw, const QString& keyfile)
144
auto db = QSharedPointer<Database>::create();
145
auto key = QSharedPointer<CompositeKey>::create();
147
if (filename.isEmpty()) {
148
// Open the default test database
149
key->addKey(QSharedPointer<PasswordKey>::create("a"));
150
if (!db->open(m_dbFile->fileName(), key)) {
154
// Open the specified database file using supplied credentials
155
key->addKey(QSharedPointer<PasswordKey>::create(pw));
156
if (!keyfile.isEmpty()) {
157
auto filekey = QSharedPointer<FileKey>::create();
158
filekey->load(keyfile);
159
key->addKey(filekey);
162
if (!db->open(filename, key)) {
170
int TestCli::execCmd(Command& cmd, const QStringList& args) const
172
// Move to end of stream
176
// Record stream position
177
auto outPos = m_stdout->pos();
178
auto errPos = m_stderr->pos();
181
int ret = cmd.execute(args);
183
// Move back to recorded position
184
m_stdout->seek(outPos);
185
m_stderr->seek(errPos);
187
// Skip over blank lines
188
QByteArray newline("\n");
189
while (m_stdout->peek(1) == newline) {
190
m_stdout->readLine();
192
while (m_stderr->peek(1) == newline) {
193
m_stderr->readLine();
199
bool TestCli::isTotp(const QString& value)
201
static const QRegularExpression totp("^\\d{6}$");
202
return totp.match(value.trimmed()).hasMatch();
205
void TestCli::setInput(const QString& input)
207
setInput(QStringList(input));
210
void TestCli::setInput(const QStringList& input)
212
auto ba = input.join("\n").toLatin1();
213
// Always end in newline
214
if (!ba.endsWith("\n")) {
217
auto pos = m_stdin->pos();
222
void TestCli::testBatchCommands()
224
Commands::setupCommands(false);
225
QVERIFY(Commands::getCommand("add"));
226
QVERIFY(Commands::getCommand("analyze"));
227
QVERIFY(Commands::getCommand("attachment-export"));
228
QVERIFY(Commands::getCommand("attachment-import"));
229
QVERIFY(Commands::getCommand("attachment-rm"));
230
QVERIFY(Commands::getCommand("clip"));
231
QVERIFY(Commands::getCommand("close"));
232
QVERIFY(Commands::getCommand("db-create"));
233
QVERIFY(Commands::getCommand("db-info"));
234
QVERIFY(Commands::getCommand("diceware"));
235
QVERIFY(Commands::getCommand("edit"));
236
QVERIFY(Commands::getCommand("estimate"));
237
QVERIFY(Commands::getCommand("export"));
238
QVERIFY(Commands::getCommand("generate"));
239
QVERIFY(Commands::getCommand("help"));
240
QVERIFY(Commands::getCommand("import"));
241
QVERIFY(Commands::getCommand("ls"));
242
QVERIFY(Commands::getCommand("merge"));
243
QVERIFY(Commands::getCommand("mkdir"));
244
QVERIFY(Commands::getCommand("mv"));
245
QVERIFY(Commands::getCommand("open"));
246
QVERIFY(Commands::getCommand("rm"));
247
QVERIFY(Commands::getCommand("rmdir"));
248
QVERIFY(Commands::getCommand("show"));
249
QVERIFY(Commands::getCommand("search"));
250
QVERIFY(!Commands::getCommand("doesnotexist"));
251
QCOMPARE(Commands::getCommands().size(), 26);
254
void TestCli::testInteractiveCommands()
256
Commands::setupCommands(true);
257
QVERIFY(Commands::getCommand("add"));
258
QVERIFY(Commands::getCommand("analyze"));
259
QVERIFY(Commands::getCommand("attachment-export"));
260
QVERIFY(Commands::getCommand("attachment-import"));
261
QVERIFY(Commands::getCommand("attachment-rm"));
262
QVERIFY(Commands::getCommand("clip"));
263
QVERIFY(Commands::getCommand("close"));
264
QVERIFY(Commands::getCommand("db-create"));
265
QVERIFY(Commands::getCommand("db-info"));
266
QVERIFY(Commands::getCommand("diceware"));
267
QVERIFY(Commands::getCommand("edit"));
268
QVERIFY(Commands::getCommand("estimate"));
269
QVERIFY(Commands::getCommand("exit"));
270
QVERIFY(Commands::getCommand("generate"));
271
QVERIFY(Commands::getCommand("help"));
272
QVERIFY(Commands::getCommand("ls"));
273
QVERIFY(Commands::getCommand("merge"));
274
QVERIFY(Commands::getCommand("mkdir"));
275
QVERIFY(Commands::getCommand("mv"));
276
QVERIFY(Commands::getCommand("open"));
277
QVERIFY(Commands::getCommand("quit"));
278
QVERIFY(Commands::getCommand("rm"));
279
QVERIFY(Commands::getCommand("rmdir"));
280
QVERIFY(Commands::getCommand("show"));
281
QVERIFY(Commands::getCommand("search"));
282
QVERIFY(!Commands::getCommand("doesnotexist"));
283
QCOMPARE(Commands::getCommands().size(), 26);
286
void TestCli::testAdd()
289
QVERIFY(!addCmd.name.isEmpty());
290
QVERIFY(addCmd.getDescriptionLine().contains(addCmd.name));
298
"https://example.com/",
304
m_dbFile->fileName(),
306
m_stderr->readLine(); // Skip password prompt
307
QCOMPARE(m_stderr->readAll(), QByteArray());
308
QVERIFY(m_stdout->readAll().contains("Successfully added entry newuser-entry."));
310
auto db = readDatabase();
311
auto* entry = db->rootGroup()->findEntryByPath("/newuser-entry");
313
QCOMPARE(entry->username(), QString("newuser"));
314
QCOMPARE(entry->url(), QString("https://example.com/"));
315
QCOMPARE(entry->password().size(), 20);
316
QCOMPARE(entry->notes(), QString("some notes"));
320
execCmd(addCmd, {"add", "-q", "-u", "newuser", "-g", "-L", "20", m_dbFile->fileName(), "/newentry-quiet"});
321
QCOMPARE(m_stderr->readAll(), QByteArray());
322
QCOMPARE(m_stdout->readAll(), QByteArray());
324
entry = db->rootGroup()->findEntryByPath("/newentry-quiet");
326
QCOMPARE(entry->password().size(), 20);
328
setInput({"a", "newpassword"});
330
{"add", "-u", "newuser2", "--url", "https://example.net/", "-p", m_dbFile->fileName(), "/newuser-entry2"});
331
QVERIFY(m_stdout->readAll().contains("Successfully added entry newuser-entry2."));
334
entry = db->rootGroup()->findEntryByPath("/newuser-entry2");
336
QCOMPARE(entry->username(), QString("newuser2"));
337
QCOMPARE(entry->url(), QString("https://example.net/"));
338
QCOMPARE(entry->password(), QString("newpassword"));
340
// Password generation options
342
execCmd(addCmd, {"add", "-u", "newuser3", "-g", "-L", "34", m_dbFile->fileName(), "/newuser-entry3"});
343
QVERIFY(m_stdout->readAll().contains("Successfully added entry newuser-entry3."));
346
entry = db->rootGroup()->findEntryByPath("/newuser-entry3");
348
QCOMPARE(entry->username(), QString("newuser3"));
349
QCOMPARE(entry->password().size(), 34);
350
QRegularExpression defaultPasswordClassesRegex("^[a-zA-Z0-9]+$");
351
QVERIFY(defaultPasswordClassesRegex.match(entry->password()).hasMatch());
366
m_dbFile->fileName(),
368
QVERIFY(m_stdout->readAll().contains("Successfully added entry newuser-entry4."));
371
entry = db->rootGroup()->findEntryByPath("/newuser-entry4");
373
QCOMPARE(entry->username(), QString("newuser4"));
374
QCOMPARE(entry->password().size(), 20);
375
QVERIFY(!defaultPasswordClassesRegex.match(entry->password()).hasMatch());
378
execCmd(addCmd, {"add", "-u", "newuser5", "--notes", "test\\nnew line", m_dbFile->fileName(), "/newuser-entry5"});
379
m_stderr->readLine(); // skip password prompt
380
QCOMPARE(m_stderr->readAll(), QByteArray(""));
381
QCOMPARE(m_stdout->readAll(), QByteArray("Successfully added entry newuser-entry5.\n"));
384
entry = db->rootGroup()->findEntryByPath("/newuser-entry5");
386
QCOMPARE(entry->username(), QString("newuser5"));
387
QCOMPARE(entry->notes(), QString("test\nnew line"));
390
void TestCli::testAddGroup()
392
AddGroup addGroupCmd;
393
QVERIFY(!addGroupCmd.name.isEmpty());
394
QVERIFY(addGroupCmd.getDescriptionLine().contains(addGroupCmd.name));
397
execCmd(addGroupCmd, {"mkdir", m_dbFile->fileName(), "/new_group"});
398
m_stderr->readLine(); // Skip password prompt
399
QCOMPARE(m_stderr->readAll(), QByteArray());
400
QCOMPARE(m_stdout->readAll(), QByteArray("Successfully added group new_group.\n"));
402
auto db = readDatabase();
403
auto* group = db->rootGroup()->findGroupByPath("new_group");
405
QCOMPARE(group->name(), QString("new_group"));
407
// Trying to add the same group should fail.
409
execCmd(addGroupCmd, {"mkdir", m_dbFile->fileName(), "/new_group"});
410
QVERIFY(m_stderr->readAll().contains("Group /new_group already exists!"));
411
QCOMPARE(m_stdout->readAll(), QByteArray());
413
// Should be able to add groups down the tree.
415
execCmd(addGroupCmd, {"mkdir", m_dbFile->fileName(), "/new_group/newer_group"});
416
QVERIFY(m_stdout->readAll().contains("Successfully added group newer_group."));
419
group = db->rootGroup()->findGroupByPath("new_group/newer_group");
421
QCOMPARE(group->name(), QString("newer_group"));
423
// Should fail if the path is invalid.
425
execCmd(addGroupCmd, {"mkdir", m_dbFile->fileName(), "/invalid_group/newer_group"});
426
QVERIFY(m_stderr->readAll().contains("Group /invalid_group not found."));
427
QCOMPARE(m_stdout->readAll(), QByteArray());
429
// Should fail to add the root group.
431
execCmd(addGroupCmd, {"mkdir", m_dbFile->fileName(), "/"});
432
QVERIFY(m_stderr->readAll().contains("Group / already exists!"));
433
QCOMPARE(m_stdout->readAll(), QByteArray());
436
void TestCli::testAnalyze()
439
QVERIFY(!analyzeCmd.name.isEmpty());
440
QVERIFY(analyzeCmd.getDescriptionLine().contains(analyzeCmd.name));
442
const QString hibpPath = QString(KEEPASSX_TEST_DATA_DIR).append("/hibp.txt");
445
execCmd(analyzeCmd, {"analyze", "--hibp", hibpPath, m_dbFile->fileName()});
446
auto output = m_stdout->readAll();
447
QVERIFY(output.contains("Sample Entry"));
448
QVERIFY(output.contains("123"));
449
m_stderr->readLine(); // Skip password prompt
450
QCOMPARE(m_stderr->readAll(), QByteArray());
453
void TestCli::testAttachmentExport()
455
AttachmentExport attachmentExportCmd;
456
QVERIFY(!attachmentExportCmd.name.isEmpty());
457
QVERIFY(attachmentExportCmd.getDescriptionLine().contains(attachmentExportCmd.name));
459
TemporaryFile exportOutput;
460
exportOutput.open(QIODevice::WriteOnly);
461
exportOutput.close();
463
// Try exporting an attachment of a non-existent entry
465
execCmd(attachmentExportCmd,
466
{"attachment-export",
467
m_dbFile->fileName(),
468
"invalid_entry_path",
469
"invalid_attachment_name",
470
exportOutput.fileName()});
471
m_stderr->readLine(); // skip password prompt
472
QCOMPARE(m_stderr->readAll(), QByteArray("Could not find entry with path invalid_entry_path.\n"));
473
QCOMPARE(m_stdout->readAll(), QByteArray());
475
// Try exporting a non-existent attachment
477
execCmd(attachmentExportCmd,
478
{"attachment-export",
479
m_dbFile->fileName(),
481
"invalid_attachment_name",
482
exportOutput.fileName()});
483
m_stderr->readLine(); // skip password prompt
484
QCOMPARE(m_stderr->readAll(), QByteArray("Could not find attachment with name invalid_attachment_name.\n"));
485
QCOMPARE(m_stdout->readAll(), QByteArray());
487
// Export an existing attachment to a file
491
{"attachment-export", m_dbFile->fileName(), "/Sample Entry", "Sample attachment.txt", exportOutput.fileName()});
492
m_stderr->readLine(); // skip password prompt
493
QCOMPARE(m_stderr->readAll(), QByteArray());
494
QCOMPARE(m_stdout->readAll(),
495
QByteArray(qPrintable(QString("Successfully exported attachment %1 of entry %2 to %3.\n")
496
.arg("Sample attachment.txt", "/Sample Entry", exportOutput.fileName()))));
498
exportOutput.open(QIODevice::ReadOnly);
499
QCOMPARE(exportOutput.readAll(), QByteArray("Sample content\n"));
501
// Export an existing attachment to stdout
503
execCmd(attachmentExportCmd,
504
{"attachment-export", "--stdout", m_dbFile->fileName(), "/Sample Entry", "Sample attachment.txt"});
505
m_stderr->readLine(); // skip password prompt
506
QCOMPARE(m_stderr->readAll(), QByteArray());
507
QCOMPARE(m_stdout->readAll(), QByteArray("Sample content\n"));
509
// Ensure --stdout works even in quiet mode
513
{"attachment-export", "--quiet", "--stdout", m_dbFile->fileName(), "/Sample Entry", "Sample attachment.txt"});
514
m_stderr->readLine(); // skip password prompt
515
QCOMPARE(m_stderr->readAll(), QByteArray());
516
QCOMPARE(m_stdout->readAll(), QByteArray("Sample content\n"));
519
void TestCli::testAttachmentImport()
521
AttachmentImport attachmentImportCmd;
522
QVERIFY(!attachmentImportCmd.name.isEmpty());
523
QVERIFY(attachmentImportCmd.getDescriptionLine().contains(attachmentImportCmd.name));
525
const QString attachmentPath = QString(KEEPASSX_TEST_DATA_DIR).append("/Attachment.txt");
526
QVERIFY(QFile::exists(attachmentPath));
528
// Try importing an attachment to a non-existent entry
530
execCmd(attachmentImportCmd,
531
{"attachment-import",
532
m_dbFile->fileName(),
533
"invalid_entry_path",
534
"invalid_attachment_name",
535
"invalid_attachment_path"});
536
m_stderr->readLine(); // skip password prompt
537
QCOMPARE(m_stderr->readAll(), QByteArray("Could not find entry with path invalid_entry_path.\n"));
538
QCOMPARE(m_stdout->readAll(), QByteArray());
540
// Try importing an attachment with an occupied name without -f option
542
execCmd(attachmentImportCmd,
543
{"attachment-import",
544
m_dbFile->fileName(),
546
"Sample attachment.txt",
547
"invalid_attachment_path"});
548
m_stderr->readLine(); // skip password prompt
549
QCOMPARE(m_stderr->readAll(),
550
QByteArray("Attachment Sample attachment.txt already exists for entry /Sample Entry.\n"));
551
QCOMPARE(m_stdout->readAll(), QByteArray());
553
// Try importing a non-existent attachment
555
execCmd(attachmentImportCmd,
556
{"attachment-import",
557
m_dbFile->fileName(),
559
"Sample attachment 2.txt",
560
"invalid_attachment_path"});
561
m_stderr->readLine(); // skip password prompt
562
QCOMPARE(m_stderr->readAll(), QByteArray("Could not open attachment file invalid_attachment_path.\n"));
563
QCOMPARE(m_stdout->readAll(), QByteArray());
565
// Try importing an attachment with an occupied name with -f option
569
{"attachment-import", "-f", m_dbFile->fileName(), "/Sample Entry", "Sample attachment.txt", attachmentPath});
570
m_stderr->readLine(); // skip password prompt
571
QCOMPARE(m_stderr->readAll(), QByteArray());
572
QCOMPARE(m_stdout->readAll(),
573
QByteArray(qPrintable(
574
QString("Successfully imported attachment %1 as Sample attachment.txt to entry /Sample Entry.\n")
575
.arg(attachmentPath))));
577
// Try importing an attachment with an unoccupied name
579
execCmd(attachmentImportCmd,
580
{"attachment-import", m_dbFile->fileName(), "/Sample Entry", "Attachment.txt", attachmentPath});
581
m_stderr->readLine(); // skip password prompt
582
QCOMPARE(m_stderr->readAll(), QByteArray());
585
QByteArray(qPrintable(QString("Successfully imported attachment %1 as Attachment.txt to entry /Sample Entry.\n")
586
.arg(attachmentPath))));
589
void TestCli::testAttachmentRemove()
591
AttachmentRemove attachmentRemoveCmd;
592
QVERIFY(!attachmentRemoveCmd.name.isEmpty());
593
QVERIFY(attachmentRemoveCmd.getDescriptionLine().contains(attachmentRemoveCmd.name));
595
// Try deleting an attachment belonging to an non-existent entry
597
execCmd(attachmentRemoveCmd,
598
{"attachment-rm", m_dbFile->fileName(), "invalid_entry_path", "invalid_attachment_name"});
599
m_stderr->readLine(); // skip password prompt
600
QCOMPARE(m_stderr->readAll(), QByteArray("Could not find entry with path invalid_entry_path.\n"));
601
QCOMPARE(m_stdout->readAll(), QByteArray());
603
// Try deleting a non-existent attachment from an entry
605
execCmd(attachmentRemoveCmd, {"attachment-rm", m_dbFile->fileName(), "/Sample Entry", "invalid_attachment_name"});
606
m_stderr->readLine(); // skip password prompt
607
QCOMPARE(m_stderr->readAll(), QByteArray("Could not find attachment with name invalid_attachment_name.\n"));
608
QCOMPARE(m_stdout->readAll(), QByteArray());
610
// Finally delete an existing attachment from an existing entry
611
auto db = readDatabase();
614
const Entry* entry = db->rootGroup()->findEntryByPath("/Sample Entry");
617
QVERIFY(entry->attachments()->hasKey("Sample attachment.txt"));
620
execCmd(attachmentRemoveCmd, {"attachment-rm", m_dbFile->fileName(), "/Sample Entry", "Sample attachment.txt"});
621
m_stderr->readLine(); // skip password prompt
622
QCOMPARE(m_stderr->readAll(), QByteArray());
623
QCOMPARE(m_stdout->readAll(),
624
QByteArray("Successfully removed attachment Sample attachment.txt from entry /Sample Entry.\n"));
628
QVERIFY(!db->rootGroup()->findEntryByPath("/Sample Entry")->attachments()->hasKey("Sample attachment.txt"));
631
void TestCli::testClip()
633
if (QProcessEnvironment::systemEnvironment().contains("WAYLAND_DISPLAY")) {
634
QSKIP("Clip test skipped due to QClipboard and Wayland issues on Linux");
637
QClipboard* clipboard = QGuiApplication::clipboard();
641
QVERIFY(!clipCmd.name.isEmpty());
642
QVERIFY(clipCmd.getDescriptionLine().contains(clipCmd.name));
646
execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry", "0"});
647
QString errorOutput(m_stderr->readAll());
649
if (errorOutput.contains("Unable to start program")
650
|| errorOutput.contains("No program defined for clipboard manipulation")) {
651
QSKIP("Clip test skipped due to missing clipboard tool");
653
QVERIFY(!errorOutput.contains("All clipping programs failed"));
655
m_stderr->readLine(); // Skip password prompt
656
QCOMPARE(m_stderr->readAll(), QByteArray());
657
QTRY_COMPARE(clipboard->text(), QString("Password"));
658
QCOMPARE(m_stdout->readLine(), QByteArray("Entry's \"Password\" attribute copied to the clipboard!\n"));
662
execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry", "0", "-q"});
663
QCOMPARE(m_stderr->readAll(), QByteArray());
664
QCOMPARE(m_stdout->readAll(), QByteArray());
665
QTRY_COMPARE(clipboard->text(), QString("Password"));
669
execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry", "0", "-a", "username"});
670
QTRY_COMPARE(clipboard->text(), QString("User Name"));
672
// Uuid (top-level field)
674
execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry", "0", "-a", "Uuid"});
675
QTRY_COMPARE(clipboard->text(), QString("{9f4544c2-ab00-c74a-8a1a-6eaf26cf57e9}"));
679
execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry", "0", "--totp"});
680
QTRY_VERIFY(isTotp(clipboard->text()));
681
QCOMPARE(m_stdout->readLine(), QByteArray("Entry's \"totp\" attribute copied to the clipboard!\n"));
685
execCmd(clipCmd, {"clip", m_dbFile2->fileName(), "/General/Unicode", "0", "-a", "username"});
686
QTRY_COMPARE(clipboard->text(), QString(R"(¯\_(ツ)_/¯)"));
688
// Password with timeout
691
QFuture<void> future = QtConcurrent::run(&clipCmd,
692
static_cast<int(Clip::*)(const QStringList&)>(&DatabaseCommand::execute),
693
QStringList{"clip", m_dbFile->fileName(), "/Sample Entry", "1"});
696
QTRY_COMPARE(clipboard->text(), QString("Password"));
697
QTRY_COMPARE_WITH_TIMEOUT(clipboard->text(), QString(""), 3000);
699
future.waitForFinished();
703
future = QtConcurrent::run(&clipCmd,
704
static_cast<int (Clip::*)(const QStringList&)>(&DatabaseCommand::execute),
705
QStringList{"clip", m_dbFile->fileName(), "/Sample Entry", "1", "-t"});
707
QTRY_VERIFY(isTotp(clipboard->text()));
708
QTRY_COMPARE_WITH_TIMEOUT(clipboard->text(), QString(""), 3000);
710
future.waitForFinished();
713
execCmd(clipCmd, {"clip", m_dbFile->fileName(), "--totp", "/Sample Entry", "bleuh"});
714
QVERIFY(m_stderr->readAll().contains("Invalid timeout value bleuh.\n"));
717
execCmd(clipCmd, {"clip", m_dbFile2->fileName(), "--totp", "/Sample Entry", "0"});
718
QVERIFY(m_stderr->readAll().contains("Entry with path /Sample Entry has no TOTP set up.\n"));
721
execCmd(clipCmd, {"clip", m_dbFile->fileName(), "-a", "TESTAttribute1", "/Sample Entry", "0"});
722
QVERIFY(m_stderr->readAll().contains("ERROR: attribute TESTAttribute1 is ambiguous"));
725
execCmd(clipCmd, {"clip", m_dbFile2->fileName(), "--attribute", "Username", "--totp", "/Sample Entry", "0"});
726
QVERIFY(m_stderr->readAll().contains("ERROR: Please specify one of --attribute or --totp, not both.\n"));
730
execCmd(clipCmd, {"clip", m_dbFileMulti->fileName(), "Multi", "0", "-b"});
731
QByteArray errorChoices = m_stderr->readAll();
732
QVERIFY(errorChoices.contains("Multi Entry 1"));
733
QVERIFY(errorChoices.contains("Multi Entry 2"));
736
execCmd(clipCmd, {"clip", m_dbFileMulti->fileName(), "Entry 2", "0", "-b"});
737
QTRY_COMPARE(clipboard->text(), QString("Password2"));
740
void TestCli::testCreate()
742
DatabaseCreate createCmd;
743
QVERIFY(!createCmd.name.isEmpty());
744
QVERIFY(createCmd.getDescriptionLine().contains(createCmd.name));
746
QScopedPointer<QTemporaryDir> testDir(new QTemporaryDir());
749
// Testing password option, password mismatch
750
dbFilename = testDir->path() + "/testCreate_pw.kdbx";
751
setInput({"a", "b"});
752
execCmd(createCmd, {"db-create", dbFilename, "-p"});
754
QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
755
QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
756
QCOMPARE(m_stderr->readLine(), QByteArray("Error: Passwords do not match.\n"));
757
QCOMPARE(m_stderr->readLine(), QByteArray("Failed to set database password.\n"));
759
// Testing password option
760
setInput({"a", "a"});
761
execCmd(createCmd, {"db-create", dbFilename, "-p"});
763
QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
764
QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
765
QCOMPARE(m_stdout->readLine(), QByteArray("Successfully created new database.\n"));
767
auto db = readDatabase(dbFilename, "a");
770
// Testing with empty password (deny it)
771
dbFilename = testDir->path() + "/testCreate_blankpw.kdbx";
773
execCmd(createCmd, {"db-create", dbFilename, "-p"});
775
QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
776
QVERIFY(m_stderr->readLine().contains("empty password"));
777
QCOMPARE(m_stderr->readLine(), QByteArray("Failed to set database password.\n"));
779
// Testing with empty password (accept it)
781
execCmd(createCmd, {"db-create", dbFilename, "-p"});
783
QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
784
QVERIFY(m_stderr->readLine().contains("empty password"));
785
QCOMPARE(m_stdout->readLine(), QByteArray("Successfully created new database.\n"));
787
db = readDatabase(dbFilename, "");
790
// Should refuse to create the database if it already exists.
791
execCmd(createCmd, {"db-create", dbFilename, "-p"});
792
// Output should be empty when there is an error.
793
QCOMPARE(m_stdout->readAll(), QByteArray());
794
QString errorMessage = QString("File " + dbFilename + " already exists.\n");
795
QCOMPARE(m_stderr->readAll(), errorMessage.toUtf8());
797
// Should refuse to create without any key provided.
798
dbFilename = testDir->path() + "/testCreate_key.kdbx";
799
execCmd(createCmd, {"db-create", dbFilename});
800
QCOMPARE(m_stdout->readAll(), QByteArray());
801
QCOMPARE(m_stderr->readLine(), QByteArray("No key is set. Aborting database creation.\n"));
803
// Testing with keyfile creation
804
dbFilename = testDir->path() + "/testCreate_key2.kdbx";
805
QString keyfilePath = testDir->path() + "/keyfile.txt";
806
setInput({"a", "a"});
807
execCmd(createCmd, {"db-create", dbFilename, "-p", "-k", keyfilePath});
809
QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
810
QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
811
QCOMPARE(m_stdout->readLine(), QByteArray("Successfully created new database.\n"));
813
db = readDatabase(dbFilename, "a", keyfilePath);
816
// Testing with existing keyfile
817
dbFilename = testDir->path() + "/testCreate_key3.kdbx";
818
setInput({"a", "a"});
819
execCmd(createCmd, {"db-create", dbFilename, "-p", "-k", keyfilePath});
821
QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
822
QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
823
QCOMPARE(m_stdout->readLine(), QByteArray("Successfully created new database.\n"));
825
db = readDatabase(dbFilename, "a", keyfilePath);
828
// Invalid decryption time (format).
829
dbFilename = testDir->path() + "/testCreate_time.kdbx";
830
execCmd(createCmd, {"db-create", dbFilename, "-p", "-t", "NAN"});
832
QCOMPARE(m_stdout->readAll(), QByteArray());
833
QCOMPARE(m_stderr->readAll(), QByteArray("Invalid decryption time NAN.\n"));
835
// Invalid decryption time (range).
836
execCmd(createCmd, {"db-create", dbFilename, "-p", "-t", "10"});
838
QCOMPARE(m_stdout->readAll(), QByteArray());
839
QVERIFY(m_stderr->readAll().contains(QByteArray("Target decryption time must be between")));
841
int encryptionTime = 500;
842
// Custom encryption time
843
setInput({"a", "a"});
844
int epochBefore = QDateTime::currentMSecsSinceEpoch();
845
execCmd(createCmd, {"db-create", dbFilename, "-p", "-t", QString::number(encryptionTime)});
846
// Removing 100ms to make sure we account for changes in computation time.
847
QVERIFY(QDateTime::currentMSecsSinceEpoch() > (epochBefore + encryptionTime - 100));
849
QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
850
QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
851
QCOMPARE(m_stdout->readLine(), QByteArray("Benchmarking key derivation function for 500ms delay.\n"));
852
QVERIFY(m_stdout->readLine().contains(QByteArray("rounds for key derivation function.\n")));
854
db = readDatabase(dbFilename, "a");
858
void TestCli::testDatabaseEdit()
860
TemporaryFile firstKeyFile;
862
firstKeyFile.write(QString("keyFilePassword").toLatin1());
863
firstKeyFile.close();
865
TemporaryFile secondKeyFile;
866
secondKeyFile.open();
867
secondKeyFile.write(QString("newKeyFilePassword").toLatin1());
868
secondKeyFile.close();
870
QScopedPointer<QTemporaryDir> testDir(new QTemporaryDir());
872
DatabaseCreate createCmd;
873
DatabaseEdit editCmd;
874
QVERIFY(!editCmd.name.isEmpty());
875
QVERIFY(editCmd.getDescriptionLine().contains(editCmd.name));
878
dbFilename = testDir->path() + "/testDatabaseEdit.kdbx";
880
// Creating a database for testing
881
setInput({"a", "a"});
882
execCmd(createCmd, {"db-create", dbFilename, "-p"});
883
QCOMPARE(m_stdout->readLine(), QByteArray("Successfully created new database.\n"));
886
auto db = readDatabase(dbFilename, "a");
887
QVERIFY(!db.isNull());
890
execCmd(editCmd, {"db-edit", dbFilename, "-p", "--unset-password"});
891
QCOMPARE(m_stdout->readAll(), QByteArray(""));
892
m_stderr->readLine();
893
QCOMPARE(m_stderr->readAll(), QByteArray("Cannot use p and unset-password at the same time.\n"));
896
execCmd(editCmd, {"db-edit", dbFilename, "--set-key-file", "/key/file/path", "--unset-key-file"});
897
QCOMPARE(m_stdout->readAll(), QByteArray(""));
898
// Skipping the password prompt.
899
m_stderr->readLine();
900
QCOMPARE(m_stderr->readAll(), QByteArray("Cannot use set-key-file and unset-key-file at the same time.\n"));
903
db = readDatabase(dbFilename, "a");
904
QVERIFY(!db.isNull());
906
setInput({"a", "b", "b"});
907
execCmd(editCmd, {"db-edit", dbFilename, "-p"});
908
QCOMPARE(m_stdout->readAll(), QByteArray("Successfully edited the database.\n"));
911
db = readDatabase(dbFilename, "b");
912
QVERIFY(!db.isNull());
915
execCmd(editCmd, {"db-edit", dbFilename, "--set-key-file", firstKeyFile.fileName()});
916
// Skipping the password prompt.
917
m_stderr->readLine();
918
QCOMPARE(m_stderr->readAll(), QByteArray(""));
919
QCOMPARE(m_stdout->readAll(), QByteArray("Successfully edited the database.\n"));
922
db = readDatabase(dbFilename, "b");
923
QVERIFY(db.isNull());
924
db = readDatabase(dbFilename, "b", firstKeyFile.fileName());
925
QVERIFY(!db.isNull());
929
{"db-edit", dbFilename, "-k", firstKeyFile.fileName(), "--set-key-file", secondKeyFile.fileName()});
930
QCOMPARE(m_stdout->readAll(), QByteArray("Successfully edited the database.\n"));
933
db = readDatabase(dbFilename, "b", firstKeyFile.fileName());
934
QVERIFY(db.isNull());
935
db = readDatabase(dbFilename, "b", secondKeyFile.fileName());
936
QVERIFY(!db.isNull());
939
execCmd(editCmd, {"db-edit", dbFilename, "-k", secondKeyFile.fileName(), "--unset-password"});
940
// Skipping the password prompt.
941
m_stderr->readLine();
942
QCOMPARE(m_stderr->readAll(), QByteArray(""));
943
QCOMPARE(m_stdout->readAll(), QByteArray("Successfully edited the database.\n"));
950
secondKeyFile.fileName(),
952
firstKeyFile.fileName()});
953
// Skipping the password prompt.
954
m_stderr->readLine();
955
QCOMPARE(m_stderr->readAll(), QByteArray(""));
956
QCOMPARE(m_stdout->readAll(), QByteArray("Successfully edited the database.\n"));
958
setInput({"b", "b"});
959
execCmd(editCmd, {"db-edit", dbFilename, "-k", firstKeyFile.fileName(), "--no-password", "--set-password"});
960
// Skipping over the password setting prompts.
961
m_stderr->readLine();
962
m_stderr->readLine();
963
QCOMPARE(m_stderr->readAll(), QByteArray(""));
964
QCOMPARE(m_stdout->readAll(), QByteArray("Successfully edited the database.\n"));
967
execCmd(editCmd, {"db-edit", dbFilename, "-k", firstKeyFile.fileName(), "--unset-key-file"});
968
// Skipping the password prompt.
969
m_stderr->readLine();
970
QCOMPARE(m_stderr->readAll(), QByteArray(""));
971
QCOMPARE(m_stdout->readAll(), QByteArray("Successfully edited the database.\n"));
974
db = readDatabase(dbFilename, "b", firstKeyFile.fileName());
975
QVERIFY(db.isNull());
976
db = readDatabase(dbFilename, "b");
977
QVERIFY(!db.isNull());
979
// Trying to remove the key file when there is none set should
982
execCmd(editCmd, {"db-edit", dbFilename, "-p", "--unset-key-file"});
983
QCOMPARE(m_stdout->readAll(), QByteArray(""));
984
m_stderr->readLine();
985
QCOMPARE(m_stderr->readLine(), QByteArray("Cannot remove file key: The database does not have a file key.\n"));
986
QCOMPARE(m_stderr->readLine(), QByteArray("Could not change the database key.\n"));
989
execCmd(editCmd, {"db-edit", dbFilename, "--unset-password"});
990
QCOMPARE(m_stdout->readAll(), QByteArray(""));
991
// Skipping the password prompt.
992
m_stderr->readLine();
993
QCOMPARE(m_stderr->readLine(), QByteArray("Cannot remove all the keys from a database.\n"));
996
void TestCli::testInfo()
998
DatabaseInfo infoCmd;
999
QVERIFY(!infoCmd.name.isEmpty());
1000
QVERIFY(infoCmd.getDescriptionLine().contains(infoCmd.name));
1003
execCmd(infoCmd, {"db-info", m_dbFile->fileName()});
1004
m_stderr->readLine(); // Skip password prompt
1005
QCOMPARE(m_stderr->readAll(), QByteArray());
1006
QVERIFY(m_stdout->readLine().contains(QByteArray("UUID: ")));
1007
QCOMPARE(m_stdout->readLine(), QByteArray("Name: \n"));
1008
QCOMPARE(m_stdout->readLine(), QByteArray("Description: \n"));
1009
QCOMPARE(m_stdout->readLine(), QByteArray("Cipher: AES 256-bit\n"));
1010
QCOMPARE(m_stdout->readLine(), QByteArray("KDF: AES (6000 rounds)\n"));
1011
QCOMPARE(m_stdout->readLine(), QByteArray("Recycle bin is enabled.\n"));
1012
QVERIFY(m_stdout->readLine().contains(m_dbFile->fileName().toUtf8()));
1013
QVERIFY(m_stdout->readLine().contains(
1014
QByteArray("Database created: "))); // date changes often, so just test for the first part
1015
QVERIFY(m_stdout->readLine().contains(
1016
QByteArray("Last saved: "))); // date changes often, so just test for the first part
1017
QCOMPARE(m_stdout->readLine(), QByteArray("Unsaved changes: no\n"));
1018
QCOMPARE(m_stdout->readLine(), QByteArray("Number of groups: 8\n"));
1019
QCOMPARE(m_stdout->readLine(), QByteArray("Number of entries: 2\n"));
1020
QCOMPARE(m_stdout->readLine(), QByteArray("Number of expired entries: 0\n"));
1021
QCOMPARE(m_stdout->readLine(), QByteArray("Unique passwords: 2\n"));
1022
QCOMPARE(m_stdout->readLine(), QByteArray("Non-unique passwords: 0\n"));
1023
QCOMPARE(m_stdout->readLine(), QByteArray("Maximum password reuse: 1\n"));
1024
QCOMPARE(m_stdout->readLine(), QByteArray("Number of short passwords: 0\n"));
1025
QCOMPARE(m_stdout->readLine(), QByteArray("Number of weak passwords: 2\n"));
1026
QCOMPARE(m_stdout->readLine(), QByteArray("Entries excluded from reports: 0\n"));
1027
QCOMPARE(m_stdout->readLine(), QByteArray("Average password length: 11 characters\n"));
1029
// Test with quiet option.
1031
execCmd(infoCmd, {"db-info", "-q", m_dbFile->fileName()});
1032
QCOMPARE(m_stderr->readAll(), QByteArray());
1033
QVERIFY(m_stdout->readLine().contains(QByteArray("UUID: ")));
1034
QCOMPARE(m_stdout->readLine(), QByteArray("Name: \n"));
1035
QCOMPARE(m_stdout->readLine(), QByteArray("Description: \n"));
1036
QCOMPARE(m_stdout->readLine(), QByteArray("Cipher: AES 256-bit\n"));
1037
QCOMPARE(m_stdout->readLine(), QByteArray("KDF: AES (6000 rounds)\n"));
1038
QCOMPARE(m_stdout->readLine(), QByteArray("Recycle bin is enabled.\n"));
1041
void TestCli::testDiceware()
1043
Diceware dicewareCmd;
1044
QVERIFY(!dicewareCmd.name.isEmpty());
1045
QVERIFY(dicewareCmd.getDescriptionLine().contains(dicewareCmd.name));
1047
execCmd(dicewareCmd, {"diceware"});
1048
QString passphrase(m_stdout->readLine());
1049
QVERIFY(!passphrase.isEmpty());
1051
execCmd(dicewareCmd, {"diceware", "-W", "2"});
1052
passphrase = m_stdout->readLine();
1053
QCOMPARE(passphrase.split(" ").size(), 2);
1055
execCmd(dicewareCmd, {"diceware", "-W", "10"});
1056
passphrase = m_stdout->readLine();
1057
QCOMPARE(passphrase.split(" ").size(), 10);
1059
// Testing with invalid word count
1060
execCmd(dicewareCmd, {"diceware", "-W", "-10"});
1061
QCOMPARE(m_stderr->readLine(), QByteArray("Invalid word count -10\n"));
1063
// Testing with invalid word count format
1064
execCmd(dicewareCmd, {"diceware", "-W", "bleuh"});
1065
QCOMPARE(m_stderr->readLine(), QByteArray("Invalid word count bleuh\n"));
1067
TemporaryFile wordFile;
1069
for (int i = 0; i < 4500; ++i) {
1070
wordFile.write(QString("word" + QString::number(i) + "\n").toLatin1());
1074
execCmd(dicewareCmd, {"diceware", "-W", "11", "-w", wordFile.fileName()});
1075
passphrase = m_stdout->readLine();
1076
const auto words = passphrase.split(" ");
1077
QCOMPARE(words.size(), 11);
1078
QRegularExpression regex("^word\\d+$");
1079
for (const auto& word : words) {
1080
QVERIFY2(regex.match(word).hasMatch(), qPrintable("Word " + word + " was not on the word list"));
1083
TemporaryFile smallWordFile;
1084
smallWordFile.open();
1085
for (int i = 0; i < 50; ++i) {
1086
smallWordFile.write(QString("word" + QString::number(i) + "\n").toLatin1());
1088
smallWordFile.close();
1090
execCmd(dicewareCmd, {"diceware", "-W", "11", "-w", smallWordFile.fileName()});
1091
QCOMPARE(m_stderr->readLine(), QByteArray("The word list is too small (< 1000 items)\n"));
1094
void TestCli::testEdit()
1097
QVERIFY(!editCmd.name.isEmpty());
1098
QVERIFY(editCmd.getDescriptionLine().contains(editCmd.name));
1106
"https://otherurl.example.com/",
1111
m_dbFile->fileName(),
1113
QCOMPARE(m_stdout->readLine(), QByteArray("Successfully edited entry newtitle.\n"));
1115
auto db = readDatabase();
1116
auto* entry = db->rootGroup()->findEntryByPath("/newtitle");
1118
QCOMPARE(entry->username(), QString("newuser"));
1119
QCOMPARE(entry->url(), QString("https://otherurl.example.com/"));
1120
QCOMPARE(entry->password(), QString("Password"));
1121
QCOMPARE(entry->notes(), QString("newnotes"));
1125
execCmd(editCmd, {"edit", m_dbFile->fileName(), "-q", "-t", "newertitle", "/newtitle"});
1126
QCOMPARE(m_stderr->readAll(), QByteArray());
1127
QCOMPARE(m_stdout->readAll(), QByteArray());
1130
execCmd(editCmd, {"edit", "-g", m_dbFile->fileName(), "/newertitle"});
1131
db = readDatabase();
1132
entry = db->rootGroup()->findEntryByPath("/newertitle");
1134
QCOMPARE(entry->username(), QString("newuser"));
1135
QCOMPARE(entry->url(), QString("https://otherurl.example.com/"));
1136
QVERIFY(!entry->password().isEmpty());
1137
QVERIFY(entry->password() != QString("Password"));
1140
execCmd(editCmd, {"edit", "-g", "-L", "34", "-t", "evennewertitle", m_dbFile->fileName(), "/newertitle"});
1141
db = readDatabase();
1142
entry = db->rootGroup()->findEntryByPath("/evennewertitle");
1144
QCOMPARE(entry->username(), QString("newuser"));
1145
QCOMPARE(entry->url(), QString("https://otherurl.example.com/"));
1146
QVERIFY(entry->password() != QString("Password"));
1147
QCOMPARE(entry->password().size(), 34);
1148
QRegularExpression defaultPasswordClassesRegex("^[a-zA-Z0-9]+$");
1149
QVERIFY(defaultPasswordClassesRegex.match(entry->password()).hasMatch());
1162
m_dbFile->fileName(),
1163
"/evennewertitle"});
1164
QCOMPARE(m_stdout->readAll(), QByteArray("Successfully edited entry evennewertitle.\n"));
1166
db = readDatabase();
1167
entry = db->rootGroup()->findEntryByPath("/evennewertitle");
1169
QCOMPARE(entry->password().size(), 20);
1170
QVERIFY(!defaultPasswordClassesRegex.match(entry->password()).hasMatch());
1172
setInput({"a", "newpassword"});
1173
execCmd(editCmd, {"edit", "-p", m_dbFile->fileName(), "/evennewertitle"});
1174
db = readDatabase();
1176
entry = db->rootGroup()->findEntryByPath("/evennewertitle");
1178
QCOMPARE(entry->password(), QString("newpassword"));
1180
// with line break in notes
1182
execCmd(editCmd, {"edit", m_dbFile->fileName(), "--notes", "testing\\nline breaks", "/evennewertitle"});
1183
db = readDatabase();
1184
entry = db->rootGroup()->findEntryByPath("/evennewertitle");
1186
QCOMPARE(entry->notes(), QString("testing\nline breaks"));
1189
void TestCli::testEstimate_data()
1192
QTest::addColumn<QString>("input");
1193
QTest::addColumn<QStringList>("searchStrings");
1195
QTest::newRow("Dictionary")
1197
<< QStringList{"Type: Dictionary", "\tpassword"};
1199
QTest::newRow("Spatial")
1201
<< QStringList{"Type: Spatial", "\tsdfg"};
1203
QTest::newRow("Spatial(Rep)")
1205
<< QStringList{"Type: Spatial(Rep)", "\tsdfgsdfg"};
1207
QTest::newRow("Dictionary / Sequence")
1209
<< QStringList{"Type: Dictionary", "Type: Sequence", "\tpassword", "\t123"};
1211
QTest::newRow("Dict+Leet")
1213
<< QStringList{"Type: Dict+Leet", "\tp455w0rd"};
1215
QTest::newRow("Dictionary(Rep)")
1217
<< QStringList{"Type: Dictionary(Rep)", "\thellohello"};
1219
QTest::newRow("Sequence(Rep) / Dictionary")
1221
<< QStringList{"Type: Sequence(Rep)", "Type: Dictionary", "\t456456", "\tfoobar"};
1223
QTest::newRow("Bruteforce(Rep) / Bruteforce")
1225
<< QStringList{"Type: Bruteforce(Rep)", "Type: Bruteforce", "\txzxz", "\ty"};
1227
QTest::newRow("Dictionary / Date(Rep)")
1229
<< QStringList{"Type: Dictionary", "Type: Date(Rep)", "\tpass", "\t20182018"};
1231
QTest::newRow("Dictionary / Date / Bruteforce")
1233
<< QStringList{"Type: Dictionary", "Type: Date", "Type: Bruteforce", "\tmypass", "\t2018", "\t-2"};
1235
QTest::newRow("Strong Password")
1236
<< "E*!%.Qw{t.X,&bafw)\"Q!ah$%;U/"
1237
<< QStringList{"Type: Bruteforce", "\tE*"};
1239
// TODO: detect passphrases and adjust entropy calculation accordingly (issue #2347)
1240
QTest::newRow("Strong Passphrase")
1241
<< "squint wooing resupply dangle isolation axis headsman"
1242
<< QStringList{"Type: Dictionary", "Type: Bruteforce", "Multi-word extra bits 22.0", "\tsquint", "\t ", "\twooing"};
1246
void TestCli::testEstimate()
1248
QFETCH(QString, input);
1249
QFETCH(QStringList, searchStrings);
1251
// Calculate expected values since zxcvbn output can vary by platform if different wordlists are used
1252
const auto e = ZxcvbnMatch(input.toUtf8(), nullptr, nullptr);
1253
auto length = QString::number(input.length());
1254
auto entropy = QString("%1").arg(e, 0, 'f', 3);
1255
auto log10 = QString("%1").arg(e * 0.301029996, 0, 'f', 3);
1257
Estimate estimateCmd;
1258
QVERIFY(!estimateCmd.name.isEmpty());
1259
QVERIFY(estimateCmd.getDescriptionLine().contains(estimateCmd.name));
1262
execCmd(estimateCmd, {"estimate", "-a"});
1263
auto result = QString(m_stdout->readAll());
1264
QVERIFY(result.contains("Length " + length));
1265
QVERIFY(result.contains("Entropy " + entropy));
1266
QVERIFY(result.contains("Log10 " + log10));
1267
for (const auto& string : asConst(searchStrings)) {
1268
QVERIFY2(result.contains(string), qPrintable("String " + string + " missing"));
1272
void TestCli::testExport()
1275
QVERIFY(!exportCmd.name.isEmpty());
1276
QVERIFY(exportCmd.getDescriptionLine().contains(exportCmd.name));
1279
execCmd(exportCmd, {"export", m_dbFile->fileName()});
1281
TemporaryFile xmlOutput;
1282
xmlOutput.open(QIODevice::WriteOnly);
1283
xmlOutput.write(m_stdout->readAll());
1286
QScopedPointer<Database> db(new Database());
1287
QVERIFY(db->import(xmlOutput.fileName()));
1289
auto* entry = db->rootGroup()->findEntryByPath("/Sample Entry");
1291
QCOMPARE(entry->password(), QString("Password"));
1294
QScopedPointer<Database> dbQuiet(new Database());
1296
execCmd(exportCmd, {"export", "-f", "xml", "-q", m_dbFile->fileName()});
1297
QCOMPARE(m_stderr->readAll(), QByteArray());
1299
xmlOutput.open(QIODevice::WriteOnly);
1300
xmlOutput.write(m_stdout->readAll());
1303
QVERIFY(db->import(xmlOutput.fileName()));
1307
execCmd(exportCmd, {"export", "-f", "csv", m_dbFile->fileName()});
1308
QByteArray csvHeader = m_stdout->readLine();
1309
QVERIFY(csvHeader.contains(QByteArray("\"Group\",\"Title\",\"Username\",\"Password\",\"URL\",\"Notes\"")));
1310
QByteArray csvData = m_stdout->readAll();
1311
QVERIFY(csvData.contains(QByteArray(
1312
"\"NewDatabase\",\"Sample Entry\",\"User Name\",\"Password\",\"http://www.somesite.com/\",\"Notes\"")));
1314
// test invalid format
1316
execCmd(exportCmd, {"export", "-f", "yaml", m_dbFile->fileName()});
1317
m_stderr->readLine(); // Skip password prompt
1318
QCOMPARE(m_stderr->readLine(), QByteArray("Unsupported format yaml\n"));
1321
void TestCli::testGenerate_data()
1323
QTest::addColumn<QStringList>("parameters");
1324
QTest::addColumn<QString>("pattern");
1326
QTest::newRow("default") << QStringList{"generate"} << "^[^\r\n]+$";
1327
QTest::newRow("length") << QStringList{"generate", "-L", "13"} << "^.{13}$";
1328
QTest::newRow("lowercase") << QStringList{"generate", "-L", "14", "-l"} << "^[a-z]{14}$";
1329
QTest::newRow("uppercase") << QStringList{"generate", "-L", "15", "--upper"} << "^[A-Z]{15}$";
1330
QTest::newRow("numbers") << QStringList{"generate", "-L", "16", "-n"} << "^[0-9]{16}$";
1331
QTest::newRow("special") << QStringList{"generate", "-L", "200", "-s"}
1332
<< R"(^[\(\)\[\]\{\}\.\-*|\\,:;"'\/\_!+-<=>?#$%&^`@~]{200}$)";
1333
QTest::newRow("special (exclude)") << QStringList{"generate", "-L", "200", "-s", "-x", "+.?@&"}
1334
<< R"(^[\(\)\[\]\{\}\.\-*|\\,:;"'\/\_!-<=>#$%^`~]{200}$)";
1335
QTest::newRow("extended") << QStringList{"generate", "-L", "50", "-e"}
1336
<< R"(^[^a-zA-Z0-9\(\)\[\]\{\}\.\-\*\|\\,:;"'\/\_!+-<=>?#$%&^`@~]{50}$)";
1337
QTest::newRow("numbers + lowercase + uppercase")
1338
<< QStringList{"generate", "-L", "16", "-n", "--upper", "-l"} << "^[0-9a-zA-Z]{16}$";
1339
QTest::newRow("numbers + lowercase + uppercase (exclude)")
1340
<< QStringList{"generate", "-L", "500", "-n", "-U", "-l", "-x", "abcdefg0123@"} << "^[^abcdefg0123@]{500}$";
1341
QTest::newRow("numbers + lowercase + uppercase (exclude similar)")
1342
<< QStringList{"generate", "-L", "200", "-n", "-U", "-l", "--exclude-similar"} << "^[^l1IO0]{200}$";
1343
QTest::newRow("uppercase + lowercase (every)")
1344
<< QStringList{"generate", "-L", "2", "--upper", "-l", "--every-group"} << "^[a-z][A-Z]|[A-Z][a-z]$";
1345
QTest::newRow("numbers + lowercase (every)")
1346
<< QStringList{"generate", "-L", "2", "-n", "-l", "--every-group"} << "^[a-z][0-9]|[0-9][a-z]$";
1347
QTest::newRow("custom character set")
1348
<< QStringList{"generate", "-L", "200", "-n", "-c", "abc"} << "^[abc0-9]{200}$";
1349
QTest::newRow("custom character set without extra options uses only custom chars")
1350
<< QStringList{"generate", "-L", "200", "-c", "a"} << "^a{200}$";
1353
void TestCli::testGenerate()
1355
QFETCH(QStringList, parameters);
1356
QFETCH(QString, pattern);
1358
Generate generateCmd;
1359
QVERIFY(!generateCmd.name.isEmpty());
1360
QVERIFY(generateCmd.getDescriptionLine().contains(generateCmd.name));
1362
for (int i = 0; i < 10; ++i) {
1363
execCmd(generateCmd, parameters);
1364
QRegularExpression regex(pattern);
1366
QString password = QString::fromUtf8(m_stdout->readLine());
1368
QString password = QString::fromLatin1(m_stdout->readLine());
1371
QVERIFY2(regex.match(password).hasMatch(),
1372
qPrintable("Password " + password + " does not match pattern " + pattern));
1373
QCOMPARE(m_stderr->readAll(), QByteArray());
1376
// Testing with invalid password length
1377
execCmd(generateCmd, {"generate", "-L", "-10"});
1378
QCOMPARE(m_stderr->readLine(), QByteArray("Invalid password length -10\n"));
1380
execCmd(generateCmd, {"generate", "-L", "0"});
1381
QCOMPARE(m_stderr->readLine(), QByteArray("Invalid password length 0\n"));
1383
// Testing with invalid word count format
1384
execCmd(generateCmd, {"generate", "-L", "bleuh"});
1385
QCOMPARE(m_stderr->readLine(), QByteArray("Invalid password length bleuh\n"));
1388
void TestCli::testImport()
1391
QVERIFY(!importCmd.name.isEmpty());
1392
QVERIFY(importCmd.getDescriptionLine().contains(importCmd.name));
1394
QScopedPointer<QTemporaryDir> testDir(new QTemporaryDir());
1395
QString databaseFilename = testDir->path() + "/testImport1.kdbx";
1397
setInput({"a", "a"});
1398
execCmd(importCmd, {"import", m_xmlFile->fileName(), databaseFilename, "-p"});
1400
QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
1401
QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
1402
QCOMPARE(m_stdout->readLine(), QByteArray("Successfully imported database.\n"));
1404
auto db = readDatabase(databaseFilename, "a");
1406
auto* entry = db->rootGroup()->findEntryByPath("/Sample Entry 1");
1408
QCOMPARE(entry->username(), QString("User Name"));
1410
// Should refuse to create the database if it already exists.
1411
execCmd(importCmd, {"import", m_xmlFile->fileName(), databaseFilename});
1412
// Output should be empty when there is an error.
1413
QCOMPARE(m_stdout->readAll(), QByteArray());
1414
QString errorMessage = QString("File " + databaseFilename + " already exists.\n");
1415
QCOMPARE(m_stderr->readAll(), errorMessage.toUtf8());
1417
// Testing import with non-existing keyfile
1418
databaseFilename = testDir->path() + "/testImport2.kdbx";
1419
QString keyfilePath = testDir->path() + "/keyfile.txt";
1420
setInput({"a", "a"});
1421
execCmd(importCmd, {"import", "-p", "-k", keyfilePath, m_xmlFile->fileName(), databaseFilename});
1423
QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
1424
QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
1425
QCOMPARE(m_stdout->readLine(), QByteArray("Successfully imported database.\n"));
1427
db = readDatabase(databaseFilename, "a", keyfilePath);
1430
// Testing import with existing keyfile
1431
databaseFilename = testDir->path() + "/testImport3.kdbx";
1432
setInput({"a", "a"});
1433
execCmd(importCmd, {"import", "-p", "-k", keyfilePath, m_xmlFile->fileName(), databaseFilename});
1435
QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
1436
QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
1437
QCOMPARE(m_stdout->readLine(), QByteArray("Successfully imported database.\n"));
1439
db = readDatabase(databaseFilename, "a", keyfilePath);
1442
// Invalid decryption time (format).
1443
databaseFilename = testDir->path() + "/testCreate_time.kdbx";
1444
execCmd(importCmd, {"import", "-p", "-t", "NAN", m_xmlFile->fileName(), databaseFilename});
1446
QCOMPARE(m_stdout->readAll(), QByteArray());
1447
QCOMPARE(m_stderr->readAll(), QByteArray("Invalid decryption time NAN.\n"));
1449
// Invalid decryption time (range).
1450
execCmd(importCmd, {"import", "-p", "-t", "10", m_xmlFile->fileName(), databaseFilename});
1452
QCOMPARE(m_stdout->readAll(), QByteArray());
1453
QVERIFY(m_stderr->readAll().contains(QByteArray("Target decryption time must be between")));
1455
int encryptionTime = 500;
1456
// Custom encryption time
1457
setInput({"a", "a"});
1458
int epochBefore = QDateTime::currentMSecsSinceEpoch();
1460
{"import", "-p", "-t", QString::number(encryptionTime), m_xmlFile->fileName(), databaseFilename});
1461
// Removing 100ms to make sure we account for changes in computation time.
1462
QVERIFY(QDateTime::currentMSecsSinceEpoch() > (epochBefore + encryptionTime - 100));
1464
QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
1465
QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
1466
QCOMPARE(m_stdout->readLine(), QByteArray("Benchmarking key derivation function for 500ms delay.\n"));
1467
QVERIFY(m_stdout->readLine().contains(QByteArray("rounds for key derivation function.\n")));
1469
db = readDatabase(databaseFilename, "a");
1473
QScopedPointer<QTemporaryDir> testDirQuiet(new QTemporaryDir());
1474
QString databaseFilenameQuiet = testDirQuiet->path() + "/testImport2.kdbx";
1476
setInput({"a", "a"});
1477
execCmd(importCmd, {"import", "-p", "-q", m_xmlFile->fileName(), databaseFilenameQuiet});
1479
QCOMPARE(m_stderr->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
1480
QCOMPARE(m_stderr->readLine(), QByteArray("Repeat password: \n"));
1481
QCOMPARE(m_stdout->readLine(), QByteArray());
1483
db = readDatabase(databaseFilenameQuiet, "a");
1487
void TestCli::testKeyFileOption()
1491
QString keyFilePath(QString(KEEPASSX_TEST_DATA_DIR).append("/KeyFileProtected.key"));
1493
execCmd(listCmd, {"ls", "-k", keyFilePath, m_keyFileProtectedDbFile->fileName()});
1494
m_stderr->readLine(); // Skip password prompt
1495
QCOMPARE(m_stderr->readAll(), QByteArray());
1496
QCOMPARE(m_stdout->readAll(),
1497
QByteArray("entry1\n"
1500
// Should raise an error with no key file.
1502
execCmd(listCmd, {"ls", m_keyFileProtectedDbFile->fileName()});
1503
QCOMPARE(m_stdout->readAll(), QByteArray());
1504
QVERIFY(m_stderr->readAll().contains("Invalid credentials were provided"));
1506
// Should raise an error if key file path is invalid.
1508
execCmd(listCmd, {"ls", "-k", "invalidpath", m_keyFileProtectedDbFile->fileName()});
1509
m_stderr->readLine(); // skip password prompt
1510
QCOMPARE(m_stdout->readAll(), QByteArray());
1511
QCOMPARE(m_stderr->readAll().split(':').at(0), QByteArray("Failed to load key file invalidpath"));
1514
void TestCli::testNoPasswordOption()
1518
QString keyFilePath(QString(KEEPASSX_TEST_DATA_DIR).append("/KeyFileProtectedNoPassword.key"));
1519
execCmd(listCmd, {"ls", "-k", keyFilePath, "--no-password", m_keyFileProtectedNoPasswordDbFile->fileName()});
1520
// Expecting no password prompt
1521
QCOMPARE(m_stderr->readAll(), QByteArray());
1522
QCOMPARE(m_stdout->readAll(),
1523
QByteArray("entry1\n"
1526
// Should raise an error with no key file.
1527
execCmd(listCmd, {"ls", "--no-password", m_keyFileProtectedNoPasswordDbFile->fileName()});
1528
QCOMPARE(m_stdout->readAll(), QByteArray());
1529
QVERIFY(m_stderr->readAll().contains("Invalid credentials were provided"));
1532
void TestCli::testList()
1535
QVERIFY(!listCmd.name.isEmpty());
1536
QVERIFY(listCmd.getDescriptionLine().contains(listCmd.name));
1539
execCmd(listCmd, {"ls", m_dbFile->fileName()});
1540
m_stderr->readLine(); // Skip password prompt
1541
QCOMPARE(m_stderr->readAll(), QByteArray());
1542
QCOMPARE(m_stdout->readAll(),
1543
QByteArray("Sample Entry\n"
1553
execCmd(listCmd, {"ls", "-q", m_dbFile->fileName()});
1554
QCOMPARE(m_stderr->readAll(), QByteArray());
1555
QCOMPARE(m_stdout->readAll(),
1556
QByteArray("Sample Entry\n"
1565
execCmd(listCmd, {"ls", "-R", m_dbFile->fileName()});
1566
QCOMPARE(m_stdout->readAll(),
1567
QByteArray("Sample Entry\n"
1580
" Subgroup Entry\n"));
1583
execCmd(listCmd, {"ls", "-R", "-f", m_dbFile->fileName()});
1584
QCOMPARE(m_stdout->readAll(),
1585
QByteArray("Sample Entry\n"
1593
"Internet/[empty]\n"
1597
"Homebanking/Subgroup/\n"
1598
"Homebanking/Subgroup/Subgroup Entry\n"));
1601
execCmd(listCmd, {"ls", "-R", "-f", m_dbFile->fileName(), "/Homebanking"});
1602
QCOMPARE(m_stdout->readAll(),
1603
QByteArray("Subgroup/\n"
1604
"Subgroup/Subgroup Entry\n"));
1607
execCmd(listCmd, {"ls", m_dbFile->fileName(), "/General/"});
1608
QCOMPARE(m_stdout->readAll(), QByteArray("[empty]\n"));
1611
execCmd(listCmd, {"ls", m_dbFile->fileName(), "/DoesNotExist/"});
1612
m_stderr->readLine(); // skip password prompt
1613
QCOMPARE(m_stderr->readAll(), QByteArray("Cannot find group /DoesNotExist/.\n"));
1614
QCOMPARE(m_stdout->readAll(), QByteArray());
1617
void TestCli::testMerge()
1620
QVERIFY(!mergeCmd.name.isEmpty());
1621
QVERIFY(mergeCmd.getDescriptionLine().contains(mergeCmd.name));
1623
// load test database and save copies
1624
auto db = readDatabase();
1626
TemporaryFile targetFile1;
1628
targetFile1.close();
1630
TemporaryFile targetFile2;
1632
targetFile2.close();
1634
TemporaryFile targetFile3;
1636
targetFile3.close();
1638
db->saveAs(targetFile1.fileName());
1639
db->saveAs(targetFile2.fileName());
1641
// save another copy with a different password
1642
auto oldKey = db->key();
1643
auto key = QSharedPointer<CompositeKey>::create();
1644
key->addKey(QSharedPointer<PasswordKey>::create("b"));
1646
db->saveAs(targetFile3.fileName());
1648
// Restore the original password
1651
// then add a new entry to the in-memory database and save another copy
1652
auto* entry = new Entry();
1653
entry->setUuid(QUuid::createUuid());
1654
entry->setTitle("Some Website");
1655
entry->setPassword("secretsecretsecret");
1656
auto* group = db->rootGroup()->findGroupByPath("/Internet/");
1658
group->addEntry(entry);
1660
TemporaryFile sourceFile;
1663
db->saveAs(sourceFile.fileName());
1666
execCmd(mergeCmd, {"merge", "-s", targetFile1.fileName(), sourceFile.fileName()});
1667
m_stderr->readLine(); // Skip password prompt
1668
QCOMPARE(m_stderr->readAll(), QByteArray());
1669
QList<QByteArray> outLines1 = m_stdout->readAll().split('\n');
1670
QVERIFY(outLines1.at(0).contains("Overwriting Internet"));
1671
QVERIFY(outLines1.at(1).contains("Creating missing Some Website"));
1672
QCOMPARE(outLines1.at(2),
1673
QString("Successfully merged %1 into %2.").arg(sourceFile.fileName(), targetFile1.fileName()).toUtf8());
1675
auto mergedDb = QSharedPointer<Database>::create();
1676
QVERIFY(mergedDb->open(targetFile1.fileName(), oldKey));
1678
auto* entry1 = mergedDb->rootGroup()->findEntryByPath("/Internet/Some Website");
1680
QCOMPARE(entry1->title(), QString("Some Website"));
1681
QCOMPARE(entry1->password(), QString("secretsecretsecret"));
1683
// the dry run option should not modify the target database.
1685
execCmd(mergeCmd, {"merge", "--dry-run", "-s", targetFile2.fileName(), sourceFile.fileName()});
1686
QList<QByteArray> outLines2 = m_stdout->readAll().split('\n');
1687
QVERIFY(outLines2.at(0).contains("Overwriting Internet"));
1688
QVERIFY(outLines2.at(1).contains("Creating missing Some Website"));
1689
QCOMPARE(outLines2.at(2), QByteArray("Database was not modified by merge operation."));
1691
mergedDb = QSharedPointer<Database>::create();
1692
QVERIFY(mergedDb->open(targetFile2.fileName(), oldKey));
1693
entry1 = mergedDb->rootGroup()->findEntryByPath("/Internet/Some Website");
1696
// the dry run option can be used with the quiet option
1698
execCmd(mergeCmd, {"merge", "--dry-run", "-s", "-q", targetFile2.fileName(), sourceFile.fileName()});
1699
QCOMPARE(m_stderr->readAll(), QByteArray());
1700
QCOMPARE(m_stdout->readAll(), QByteArray());
1702
mergedDb = QSharedPointer<Database>::create();
1703
QVERIFY(mergedDb->open(targetFile2.fileName(), oldKey));
1704
entry1 = mergedDb->rootGroup()->findEntryByPath("/Internet/Some Website");
1707
// try again with different passwords for both files
1708
setInput({"b", "a"});
1709
execCmd(mergeCmd, {"merge", targetFile3.fileName(), sourceFile.fileName()});
1710
QList<QByteArray> outLines3 = m_stdout->readAll().split('\n');
1711
QCOMPARE(outLines3.at(2),
1712
QString("Successfully merged %1 into %2.").arg(sourceFile.fileName(), targetFile3.fileName()).toUtf8());
1714
mergedDb = QSharedPointer<Database>::create();
1715
QVERIFY(mergedDb->open(targetFile3.fileName(), key));
1717
entry1 = mergedDb->rootGroup()->findEntryByPath("/Internet/Some Website");
1719
QCOMPARE(entry1->title(), QString("Some Website"));
1720
QCOMPARE(entry1->password(), QString("secretsecretsecret"));
1722
// making sure that the message is different if the database was not
1723
// modified by the merge operation.
1725
execCmd(mergeCmd, {"merge", "-s", sourceFile.fileName(), sourceFile.fileName()});
1726
QCOMPARE(m_stdout->readAll(), QByteArray("Database was not modified by merge operation.\n"));
1730
execCmd(mergeCmd, {"merge", "-q", "-s", sourceFile.fileName(), sourceFile.fileName()});
1731
QCOMPARE(m_stderr->readAll(), QByteArray());
1732
QCOMPARE(m_stdout->readAll(), QByteArray());
1734
// Quiet option without the -s option
1735
setInput({"a", "a"});
1736
execCmd(mergeCmd, {"merge", "-q", sourceFile.fileName(), sourceFile.fileName()});
1737
QCOMPARE(m_stderr->readAll(), QByteArray());
1738
QCOMPARE(m_stdout->readAll(), QByteArray());
1741
void TestCli::testMergeWithKeys()
1743
DatabaseCreate createCmd;
1744
QVERIFY(!createCmd.name.isEmpty());
1745
QVERIFY(createCmd.getDescriptionLine().contains(createCmd.name));
1748
QVERIFY(!mergeCmd.name.isEmpty());
1749
QVERIFY(mergeCmd.getDescriptionLine().contains(mergeCmd.name));
1751
QScopedPointer<QTemporaryDir> testDir(new QTemporaryDir());
1753
QString sourceDatabaseFilename = testDir->path() + "/testSourceDatabase.kdbx";
1754
QString sourceKeyfilePath = testDir->path() + "/testSourceKeyfile.txt";
1756
QString targetDatabaseFilename = testDir->path() + "/testTargetDatabase.kdbx";
1757
QString targetKeyfilePath = testDir->path() + "/testTargetKeyfile.txt";
1759
setInput({"a", "a"});
1760
execCmd(createCmd, {"db-create", sourceDatabaseFilename, "-p", "-k", sourceKeyfilePath});
1762
setInput({"b", "b"});
1763
execCmd(createCmd, {"db-create", targetDatabaseFilename, "-p", "-k", targetKeyfilePath});
1765
auto sourceDatabase = readDatabase(sourceDatabaseFilename, "a", sourceKeyfilePath);
1766
QVERIFY(sourceDatabase);
1768
auto targetDatabase = readDatabase(targetDatabaseFilename, "b", targetKeyfilePath);
1769
QVERIFY(targetDatabase);
1771
auto* rootGroup = new Group();
1772
rootGroup->setName("root");
1773
rootGroup->setUuid(QUuid::createUuid());
1774
auto* group = new Group();
1775
group->setUuid(QUuid::createUuid());
1776
group->setParent(rootGroup);
1777
group->setName("Internet");
1779
auto* entry = new Entry();
1780
entry->setUuid(QUuid::createUuid());
1781
entry->setTitle("Some Website");
1782
entry->setPassword("secretsecretsecret");
1783
group->addEntry(entry);
1785
auto oldGroup = sourceDatabase->setRootGroup(rootGroup);
1788
auto* otherRootGroup = new Group();
1789
otherRootGroup->setName("root");
1790
otherRootGroup->setUuid(QUuid::createUuid());
1791
auto* otherGroup = new Group();
1792
otherGroup->setUuid(QUuid::createUuid());
1793
otherGroup->setParent(otherRootGroup);
1794
otherGroup->setName("Internet");
1796
auto* otherEntry = new Entry();
1797
otherEntry->setUuid(QUuid::createUuid());
1798
otherEntry->setTitle("Some Website 2");
1799
otherEntry->setPassword("secretsecretsecret 2");
1800
otherGroup->addEntry(otherEntry);
1802
oldGroup = targetDatabase->setRootGroup(otherRootGroup);
1805
sourceDatabase->saveAs(sourceDatabaseFilename);
1806
targetDatabase->saveAs(targetDatabaseFilename);
1808
setInput({"b", "a"});
1815
targetDatabaseFilename,
1816
sourceDatabaseFilename});
1818
QList<QByteArray> lines = m_stdout->readAll().split('\n');
1819
QVERIFY(lines.contains(
1820
QString("Successfully merged %1 into %2.").arg(sourceDatabaseFilename, targetDatabaseFilename).toUtf8()));
1823
void TestCli::testMove()
1826
QVERIFY(!moveCmd.name.isEmpty());
1827
QVERIFY(moveCmd.getDescriptionLine().contains(moveCmd.name));
1830
execCmd(moveCmd, {"mv", m_dbFile->fileName(), "invalid_entry_path", "invalid_group_path"});
1831
m_stderr->readLine(); // skip password prompt
1832
QCOMPARE(m_stderr->readLine(), QByteArray("Could not find entry with path invalid_entry_path.\n"));
1833
QCOMPARE(m_stdout->readLine(), QByteArray());
1836
execCmd(moveCmd, {"mv", m_dbFile->fileName(), "Sample Entry", "invalid_group_path"});
1837
m_stderr->readLine(); // skip password prompt
1838
QCOMPARE(m_stderr->readLine(), QByteArray("Could not find group with path invalid_group_path.\n"));
1839
QCOMPARE(m_stdout->readLine(), QByteArray());
1842
execCmd(moveCmd, {"mv", m_dbFile->fileName(), "Sample Entry", "General/"});
1843
m_stderr->readLine(); // skip password prompt
1844
QCOMPARE(m_stderr->readLine(), QByteArray());
1845
QCOMPARE(m_stdout->readLine(), QByteArray("Successfully moved entry Sample Entry to group General/.\n"));
1847
auto db = readDatabase();
1848
auto* entry = db->rootGroup()->findEntryByPath("General/Sample Entry");
1851
// Test that not modified if the same group is destination.
1853
execCmd(moveCmd, {"mv", m_dbFile->fileName(), "General/Sample Entry", "General/"});
1854
m_stderr->readLine(); // skip password prompt
1855
QCOMPARE(m_stderr->readLine(), QByteArray("Entry is already in group General/.\n"));
1856
QCOMPARE(m_stdout->readLine(), QByteArray());
1859
db = readDatabase();
1860
entry = db->rootGroup()->findEntryByPath("General/Sample Entry");
1864
void TestCli::testRemove()
1867
QVERIFY(!removeCmd.name.isEmpty());
1868
QVERIFY(removeCmd.getDescriptionLine().contains(removeCmd.name));
1870
// load test database and save a copy with disabled recycle bin
1871
auto db = readDatabase();
1873
TemporaryFile fileCopy;
1877
db->metadata()->setRecycleBinEnabled(false);
1878
db->saveAs(fileCopy.fileName());
1880
// delete entry and verify
1882
execCmd(removeCmd, {"rm", m_dbFile->fileName(), "/Sample Entry"});
1883
m_stderr->readLine(); // skip password prompt
1884
QCOMPARE(m_stderr->readAll(), QByteArray());
1885
QCOMPARE(m_stdout->readAll(), QByteArray("Successfully recycled entry Sample Entry.\n"));
1887
auto readBackDb = readDatabase();
1888
QVERIFY(readBackDb);
1889
QVERIFY(!readBackDb->rootGroup()->findEntryByPath("/Sample Entry"));
1890
QVERIFY(readBackDb->rootGroup()->findEntryByPath(QString("/%1/Sample Entry").arg(Group::tr("Recycle Bin"))));
1892
// try again, this time without recycle bin
1894
execCmd(removeCmd, {"rm", fileCopy.fileName(), "/Sample Entry"});
1895
m_stderr->readLine(); // skip password prompt
1896
QCOMPARE(m_stdout->readAll(), QByteArray("Successfully deleted entry Sample Entry.\n"));
1898
readBackDb = readDatabase(fileCopy.fileName(), "a");
1899
QVERIFY(readBackDb);
1900
QVERIFY(!readBackDb->rootGroup()->findEntryByPath("/Sample Entry"));
1901
QVERIFY(!readBackDb->rootGroup()->findEntryByPath(QString("/%1/Sample Entry").arg(Group::tr("Recycle Bin"))));
1903
// finally, try deleting a non-existent entry
1905
execCmd(removeCmd, {"rm", fileCopy.fileName(), "/Sample Entry"});
1906
m_stderr->readLine(); // skip password prompt
1907
QCOMPARE(m_stderr->readAll(), QByteArray("Entry /Sample Entry not found.\n"));
1908
QCOMPARE(m_stdout->readAll(), QByteArray());
1910
// try deleting a directory, should fail
1912
execCmd(removeCmd, {"rm", fileCopy.fileName(), "/General"});
1913
m_stderr->readLine(); // skip password prompt
1914
QCOMPARE(m_stderr->readAll(), QByteArray("Entry /General not found.\n"));
1915
QCOMPARE(m_stdout->readAll(), QByteArray());
1918
void TestCli::testRemoveGroup()
1920
RemoveGroup removeGroupCmd;
1921
QVERIFY(!removeGroupCmd.name.isEmpty());
1922
QVERIFY(removeGroupCmd.getDescriptionLine().contains(removeGroupCmd.name));
1924
// try deleting a directory, should recycle it first.
1926
execCmd(removeGroupCmd, {"rmdir", m_dbFile->fileName(), "/General"});
1927
m_stderr->readLine(); // skip password prompt
1928
QCOMPARE(m_stderr->readAll(), QByteArray());
1929
QCOMPARE(m_stdout->readAll(), QByteArray("Successfully recycled group /General.\n"));
1931
auto db = readDatabase();
1932
auto* group = db->rootGroup()->findGroupByPath("General");
1935
// try deleting a directory again, should delete it permanently.
1937
execCmd(removeGroupCmd, {"rmdir", m_dbFile->fileName(), "Recycle Bin/General"});
1938
m_stderr->readLine(); // skip password prompt
1939
QCOMPARE(m_stderr->readAll(), QByteArray());
1940
QCOMPARE(m_stdout->readAll(), QByteArray("Successfully deleted group Recycle Bin/General.\n"));
1942
db = readDatabase();
1943
group = db->rootGroup()->findGroupByPath("Recycle Bin/General");
1946
// try deleting an invalid group, should fail.
1948
execCmd(removeGroupCmd, {"rmdir", m_dbFile->fileName(), "invalid"});
1949
m_stderr->readLine(); // skip password prompt
1950
QCOMPARE(m_stderr->readAll(), QByteArray("Group invalid not found.\n"));
1951
QCOMPARE(m_stdout->readAll(), QByteArray());
1953
// Should fail to remove the root group.
1955
execCmd(removeGroupCmd, {"rmdir", m_dbFile->fileName(), "/"});
1956
m_stderr->readLine(); // skip password prompt
1957
QCOMPARE(m_stderr->readAll(), QByteArray("Cannot remove root group from database.\n"));
1958
QCOMPARE(m_stdout->readAll(), QByteArray());
1961
void TestCli::testRemoveQuiet()
1964
QVERIFY(!removeCmd.name.isEmpty());
1965
QVERIFY(removeCmd.getDescriptionLine().contains(removeCmd.name));
1967
// delete entry and verify
1969
execCmd(removeCmd, {"rm", "-q", m_dbFile->fileName(), "/Sample Entry"});
1970
QCOMPARE(m_stderr->readAll(), QByteArray());
1971
QCOMPARE(m_stdout->readAll(), QByteArray());
1973
auto db = readDatabase();
1976
QVERIFY(!db->rootGroup()->findEntryByPath("/Sample Entry"));
1977
QVERIFY(db->rootGroup()->findEntryByPath(QString("/%1/Sample Entry").arg(Group::tr("Recycle Bin"))));
1979
// remove the entry completely
1981
execCmd(removeCmd, {"rm", "-q", m_dbFile->fileName(), QString("/%1/Sample Entry").arg(Group::tr("Recycle Bin"))});
1982
QCOMPARE(m_stderr->readAll(), QByteArray());
1983
QCOMPARE(m_stdout->readAll(), QByteArray());
1985
db = readDatabase();
1986
QVERIFY(!db->rootGroup()->findEntryByPath("/Sample Entry"));
1987
QVERIFY(!db->rootGroup()->findEntryByPath(QString("/%1/Sample Entry").arg(Group::tr("Recycle Bin"))));
1990
void TestCli::testSearch()
1993
QVERIFY(!searchCmd.name.isEmpty());
1994
QVERIFY(searchCmd.getDescriptionLine().contains(searchCmd.name));
1997
execCmd(searchCmd, {"search", m_dbFile->fileName(), "Sample"});
1998
m_stderr->readLine(); // Skip password prompt
1999
QCOMPARE(m_stderr->readAll(), QByteArray());
2000
QCOMPARE(m_stdout->readAll(), QByteArray("/Sample Entry\n"));
2004
execCmd(searchCmd, {"search", m_dbFile->fileName(), "-q", "Sample"});
2005
QCOMPARE(m_stderr->readAll(), QByteArray());
2006
QCOMPARE(m_stdout->readAll(), QByteArray("/Sample Entry\n"));
2009
execCmd(searchCmd, {"search", m_dbFile->fileName(), "Does Not Exist"});
2010
m_stderr->readLine(); // skip password prompt
2011
QCOMPARE(m_stderr->readAll(), QByteArray("No results for that search term.\n"));
2012
QCOMPARE(m_stdout->readAll(), QByteArray());
2014
// write a modified database
2015
auto db = readDatabase();
2017
auto* group = db->rootGroup()->findGroupByPath("/General/");
2019
auto* entry = new Entry();
2020
entry->setUuid(QUuid::createUuid());
2021
entry->setTitle("New Entry");
2022
group->addEntry(entry);
2024
TemporaryFile tmpFile;
2027
db->saveAs(tmpFile.fileName());
2030
execCmd(searchCmd, {"search", tmpFile.fileName(), "title:New"});
2031
QCOMPARE(m_stdout->readAll(), QByteArray("/General/New Entry\n"));
2034
execCmd(searchCmd, {"search", tmpFile.fileName(), "title:Entry"});
2035
QCOMPARE(m_stdout->readAll(),
2036
QByteArray("/Sample Entry\n/General/New Entry\n/Homebanking/Subgroup/Subgroup Entry\n"));
2039
execCmd(searchCmd, {"search", tmpFile.fileName(), "group:General"});
2040
QCOMPARE(m_stdout->readAll(), QByteArray("/General/New Entry\n"));
2043
execCmd(searchCmd, {"search", tmpFile.fileName(), "group:NewDatabase"});
2044
QCOMPARE(m_stdout->readAll(), QByteArray("/Sample Entry\n"));
2047
execCmd(searchCmd, {"search", tmpFile.fileName(), "group:/NewDatabase"});
2048
QCOMPARE(m_stdout->readAll(),
2049
QByteArray("/Sample Entry\n/General/New Entry\n/Homebanking/Subgroup/Subgroup Entry\n"));
2052
execCmd(searchCmd, {"search", tmpFile.fileName(), "url:bank"});
2053
QCOMPARE(m_stdout->readAll(), QByteArray("/Homebanking/Subgroup/Subgroup Entry\n"));
2056
execCmd(searchCmd, {"search", tmpFile.fileName(), "u:User Name"});
2057
QCOMPARE(m_stdout->readAll(), QByteArray("/Sample Entry\n/Homebanking/Subgroup/Subgroup Entry\n"));
2060
void TestCli::testShow()
2063
QVERIFY(!showCmd.name.isEmpty());
2064
QVERIFY(showCmd.getDescriptionLine().contains(showCmd.name));
2067
execCmd(showCmd, {"show", m_dbFile->fileName(), "/Sample Entry"});
2068
m_stderr->readLine(); // Skip password prompt
2069
QCOMPARE(m_stderr->readAll(), QByteArray());
2070
QCOMPARE(m_stdout->readAll(),
2071
QByteArray("Title: Sample Entry\n"
2072
"UserName: User Name\n"
2073
"Password: PROTECTED\n"
2074
"URL: http://www.somesite.com/\n"
2076
"Uuid: {9f4544c2-ab00-c74a-8a1a-6eaf26cf57e9}\n"
2080
execCmd(showCmd, {"show", "-s", m_dbFile->fileName(), "/Sample Entry"});
2081
QCOMPARE(m_stdout->readAll(),
2082
QByteArray("Title: Sample Entry\n"
2083
"UserName: User Name\n"
2084
"Password: Password\n"
2085
"URL: http://www.somesite.com/\n"
2087
"Uuid: {9f4544c2-ab00-c74a-8a1a-6eaf26cf57e9}\n"
2091
execCmd(showCmd, {"show", m_dbFile->fileName(), "-q", "/Sample Entry"});
2092
QCOMPARE(m_stderr->readAll(), QByteArray());
2093
QCOMPARE(m_stdout->readAll(),
2094
QByteArray("Title: Sample Entry\n"
2095
"UserName: User Name\n"
2096
"Password: PROTECTED\n"
2097
"URL: http://www.somesite.com/\n"
2099
"Uuid: {9f4544c2-ab00-c74a-8a1a-6eaf26cf57e9}\n"
2103
execCmd(showCmd, {"show", m_dbFile->fileName(), "--show-attachments", "/Sample Entry"});
2104
m_stderr->readLine(); // Skip password prompt
2105
QCOMPARE(m_stderr->readAll(), QByteArray());
2106
QCOMPARE(m_stdout->readAll(),
2107
QByteArray("Title: Sample Entry\n"
2108
"UserName: User Name\n"
2109
"Password: PROTECTED\n"
2110
"URL: http://www.somesite.com/\n"
2112
"Uuid: {9f4544c2-ab00-c74a-8a1a-6eaf26cf57e9}\n"
2116
" Sample attachment.txt (15 B)\n"));
2119
execCmd(showCmd, {"show", m_dbFile->fileName(), "--show-attachments", "/Homebanking/Subgroup/Subgroup Entry"});
2120
m_stderr->readLine(); // Skip password prompt
2121
QCOMPARE(m_stderr->readAll(), QByteArray());
2122
QCOMPARE(m_stdout->readAll(),
2123
QByteArray("Title: Subgroup Entry\n"
2124
"UserName: Bank User Name\n"
2125
"Password: PROTECTED\n"
2126
"URL: https://www.bank.com\n"
2127
"Notes: Important note\n"
2128
"Uuid: {20b183fd-6878-4506-a50b-06d30792aa10}\n"
2131
"No attachments present.\n"));
2134
execCmd(showCmd, {"show", "-a", "Title", m_dbFile->fileName(), "/Sample Entry"});
2135
QCOMPARE(m_stdout->readAll(), QByteArray("Sample Entry\n"));
2138
execCmd(showCmd, {"show", "-a", "Password", m_dbFile->fileName(), "/Sample Entry"});
2139
QCOMPARE(m_stdout->readAll(), QByteArray("Password\n"));
2142
execCmd(showCmd, {"show", "-a", "Uuid", m_dbFile->fileName(), "/Sample Entry"});
2143
QCOMPARE(m_stdout->readAll(), QByteArray("{9f4544c2-ab00-c74a-8a1a-6eaf26cf57e9}\n"));
2146
execCmd(showCmd, {"show", "-a", "Title", "-a", "URL", m_dbFile->fileName(), "/Sample Entry"});
2147
QCOMPARE(m_stdout->readAll(),
2148
QByteArray("Sample Entry\n"
2149
"http://www.somesite.com/\n"));
2151
// Test case insensitivity
2153
execCmd(showCmd, {"show", "-a", "TITLE", "-a", "URL", m_dbFile->fileName(), "/Sample Entry"});
2154
QCOMPARE(m_stdout->readAll(),
2155
QByteArray("Sample Entry\n"
2156
"http://www.somesite.com/\n"));
2159
execCmd(showCmd, {"show", "-a", "DoesNotExist", m_dbFile->fileName(), "/Sample Entry"});
2160
QCOMPARE(m_stdout->readAll(), QByteArray());
2161
QVERIFY(m_stderr->readAll().contains("ERROR: unknown attribute DoesNotExist.\n"));
2164
execCmd(showCmd, {"show", "-t", m_dbFile->fileName(), "/Sample Entry"});
2165
QVERIFY(isTotp(m_stdout->readAll()));
2168
execCmd(showCmd, {"show", "-a", "Title", m_dbFile->fileName(), "--totp", "/Sample Entry"});
2169
QCOMPARE(m_stdout->readLine(), QByteArray("Sample Entry\n"));
2170
QVERIFY(isTotp(m_stdout->readAll()));
2173
execCmd(showCmd, {"show", m_dbFile2->fileName(), "--totp", "/Sample Entry"});
2174
QCOMPARE(m_stdout->readAll(), QByteArray());
2175
QVERIFY(m_stderr->readAll().contains("Entry with path /Sample Entry has no TOTP set up.\n"));
2177
// Show with ambiguous attributes
2179
execCmd(showCmd, {"show", m_dbFile->fileName(), "-a", "Testattribute1", "/Sample Entry"});
2180
QCOMPARE(m_stdout->readAll(), QByteArray());
2181
QVERIFY(m_stderr->readAll().contains("ERROR: attribute Testattribute1 is ambiguous"));
2184
execCmd(showCmd, {"show", "--all", m_dbFile->fileName(), "/Sample Entry"});
2185
QCOMPARE(m_stdout->readAll(),
2186
QByteArray("Title: Sample Entry\n"
2187
"UserName: User Name\n"
2188
"Password: PROTECTED\n"
2189
"URL: http://www.somesite.com/\n"
2191
"Uuid: {9f4544c2-ab00-c74a-8a1a-6eaf26cf57e9}\n"
2193
"TOTP Seed: PROTECTED\n"
2194
"TOTP Settings: 30;6\n"
2195
"TestAttribute1: b\n"
2196
"testattribute1: a\n"));
2199
void TestCli::testInvalidDbFiles()
2202
QString nonExistentDbPath("/foo/bar/baz");
2203
QString directoryName("/");
2205
execCmd(showCmd, {"show", nonExistentDbPath, "/Sample Entry"});
2206
QCOMPARE(QString(m_stderr->readAll()),
2207
QObject::tr("Failed to open database file %1: not found").arg(nonExistentDbPath) + "\n");
2208
QCOMPARE(m_stdout->readAll(), QByteArray());
2210
execCmd(showCmd, {"show", directoryName, "whatever"});
2211
QCOMPARE(QString(m_stderr->readAll()),
2212
QObject::tr("Failed to open database file %1: not a plain file").arg(directoryName) + "\n");
2214
// Create a write-only file and try to open it.
2215
// QFileInfo.isReadable returns 'true' on Windows, even after the call to
2216
// setPermissions(WriteOwner) and with NTFS permissions enabled, so this
2217
// check doesn't work.
2218
#if !defined(Q_OS_WIN)
2219
QTemporaryFile tempFile;
2220
QVERIFY(tempFile.open());
2221
QString path = QFileInfo(tempFile).absoluteFilePath();
2222
QVERIFY(tempFile.setPermissions(QFileDevice::WriteOwner));
2223
execCmd(showCmd, {"show", path, "some entry"});
2224
QCOMPARE(QString(m_stderr->readAll()),
2225
QObject::tr("Failed to open database file %1: not readable").arg(path) + "\n");
2230
* Secret key for the YubiKey slot used by the unit test is
2231
* 1c e3 0f d7 8d 20 dc fa 40 b5 0c 18 77 9a fb 0f 02 28 8d b7
2232
* This secret can be on either slot but must be passive.
2234
void TestCli::testYubiKeyOption()
2236
if (!YubiKey::instance()->isInitialized()) {
2237
QSKIP("Unable to initialize YubiKey interface.");
2240
YubiKey::instance()->findValidKeys();
2242
const auto keys = YubiKey::instance()->foundKeys().keys();
2243
if (keys.isEmpty()) {
2244
QSKIP("No YubiKey devices were detected.");
2247
bool wouldBlock = false;
2248
QByteArray challenge("CLITest");
2249
Botan::secure_vector<char> response;
2250
QByteArray expected("\xA2\x3B\x94\x00\xBE\x47\x9A\x30\xA9\xEB\x50\x9B\x85\x56\x5B\x6B\x30\x25\xB4\x8E", 20);
2252
// Find a key that as configured for this test
2253
YubiKeySlot pKey(0, 0);
2254
for (auto key : keys) {
2255
if (YubiKey::instance()->testChallenge(key, &wouldBlock) && !wouldBlock) {
2256
YubiKey::instance()->challenge(key, challenge, response);
2257
if (std::memcmp(response.data(), expected.data(), expected.size()) == 0) {
2265
if (pKey.first == 0 && pKey.second == 0) {
2266
QSKIP("No YubiKey is properly configured to perform this test.");
2276
QString("%1:%2").arg(QString::number(pKey.second), QString::number(pKey.first)),
2277
m_yubiKeyProtectedDbFile->fileName()});
2278
m_stderr->readLine(); // skip password prompt
2279
QCOMPARE(m_stderr->readAll(), QByteArray());
2280
QCOMPARE(m_stdout->readAll(),
2281
QByteArray("entry1\n"
2284
// Should raise an error with no yubikey slot.
2286
execCmd(listCmd, {"ls", m_yubiKeyProtectedDbFile->fileName()});
2287
m_stderr->readLine(); // skip password prompt
2288
QCOMPARE(m_stderr->readLine(),
2289
QByteArray("Error while reading the database: Invalid credentials were provided, please try again.\n"));
2290
QCOMPARE(m_stderr->readLine(),
2291
QByteArray("If this reoccurs, then your database file may be corrupt. (HMAC mismatch)\n"));
2292
QCOMPARE(m_stdout->readAll(), QByteArray());
2294
// Should raise an error if yubikey slot is not a string
2296
execCmd(listCmd, {"ls", "-y", "invalidslot", m_yubiKeyProtectedDbFile->fileName()});
2297
m_stderr->readLine(); // skip password prompt
2298
QCOMPARE(m_stderr->readAll().split(':').at(0), QByteArray("Invalid YubiKey slot invalidslot\n"));
2299
QCOMPARE(m_stdout->readAll(), QByteArray());
2301
// Should raise an error if yubikey slot is invalid.
2303
execCmd(listCmd, {"ls", "-y", "3", m_yubiKeyProtectedDbFile->fileName()});
2304
m_stderr->readLine(); // skip password prompt
2305
QCOMPARE(m_stderr->readAll().split(':').at(0), QByteArray("Invalid YubiKey slot 3\n"));
2306
QCOMPARE(m_stdout->readAll(), QByteArray());
2309
void TestCli::testNonAscii()
2312
process.setProcessChannelMode(QProcess::MergedChannels);
2316
{"show", "-a", "password", m_nonAsciiDbFile->fileName(), QString::fromUtf8("\xe7\xa7\x98\xe5\xaf\x86")}));
2317
process.waitForStarted();
2318
QCOMPARE(process.state(), QProcess::ProcessState::Running);
2321
process.write("\xce\x94\xc3\xb6\xd8\xb6\n");
2322
process.closeWriteChannel();
2324
process.waitForFinished();
2326
process.readLine(); // skip password prompt
2327
QByteArray password = process.readLine();
2328
QCOMPARE(QString::fromUtf8(password).trimmed(),
2329
QString::fromUtf8("\xf0\x9f\x9a\x97\xf0\x9f\x90\x8e\xf0\x9f\x94\x8b\xf0\x9f\x93\x8e"));
2332
void TestCli::testCommandParsing_data()
2334
QTest::addColumn<QString>("input");
2335
QTest::addColumn<QStringList>("expectedOutput");
2337
QTest::newRow("basic") << "hello world" << QStringList({"hello", "world"});
2338
QTest::newRow("basic escaping") << "hello\\ world" << QStringList({"hello world"});
2339
QTest::newRow("quoted string") << "\"hello world\"" << QStringList({"hello world"});
2340
QTest::newRow("multiple params") << "show Passwords/Internet" << QStringList({"show", "Passwords/Internet"});
2341
QTest::newRow("quoted string inside param")
2342
<< R"(ls foo\ bar\ baz"quoted")" << QStringList({"ls", "foo bar baz\"quoted\""});
2343
QTest::newRow("multiple whitespace") << "hello world" << QStringList({"hello", "world"});
2344
QTest::newRow("single slash char") << "\\" << QStringList({"\\"});
2345
QTest::newRow("double backslash entry name") << "show foo\\\\\\\\bar" << QStringList({"show", "foo\\\\bar"});
2348
void TestCli::testCommandParsing()
2350
QFETCH(QString, input);
2351
QFETCH(QStringList, expectedOutput);
2353
QStringList result = Utils::splitCommandString(input);
2354
QCOMPARE(result.size(), expectedOutput.size());
2355
for (int i = 0; i < expectedOutput.size(); ++i) {
2356
QCOMPARE(result[i], expectedOutput[i]);
2360
void TestCli::testOpen()
2365
execCmd(openCmd, {"open", m_dbFile->fileName()});
2366
QVERIFY(openCmd.currentDatabase);
2369
// Set a current database, simulating interactive mode.
2370
listCmd.currentDatabase = openCmd.currentDatabase;
2371
execCmd(listCmd, {"ls"});
2372
QByteArray expectedOutput("Sample Entry\n"
2379
QByteArray actualOutput = m_stdout->readAll();
2380
actualOutput.truncate(expectedOutput.length());
2381
QCOMPARE(actualOutput, expectedOutput);
2384
void TestCli::testHelp()
2387
Commands::setupCommands(false);
2389
execCmd(helpCmd, {"help"});
2390
QVERIFY(m_stdout->readAll().contains("Available commands"));
2393
execCmd(helpCmd, {"help", "ls"});
2394
QVERIFY(m_stdout->readAll().contains(listCmd.description.toLatin1()));