Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/keepassxreboot/keepassxc.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJanek Bevendorff <janek@jbev.net>2018-09-29 20:00:47 +0300
committerJanek Bevendorff <janek@jbev.net>2018-10-19 22:49:54 +0300
commit113c8eb702dbdfefaf23a76bfe69603b54522b28 (patch)
tree32cbffa6b7368f5f69c9092cbbed2d2031b76800 /tests/TestCli.cpp
parent18b22834c1d9657ab98ca5160571e51837bb6366 (diff)
Add CLI tests and improve coding style and i18n
The CLI module was lacking unit test coverage and showed some severe coding style violations, which this patch addresses. In addition, all uses of qCritical() with untranslatble raw char* sequences were removed in favor of proper locale strings. These are written to STDERR through QTextStreams and support output redirection for testing purposes. With this change, error messages don't depend on the global Qt logging settings and targets anymore and go directly to the terminal or into a file if needed. This patch also fixes a bug discovered during unit test development, where the extract command would just dump the raw XML contents without decrypting embedded Salsa20-protected values first, making the XML export mostly useless, since passwords are scrambled. Lastly, all CLI commands received a dedicated -h/--help option.
Diffstat (limited to 'tests/TestCli.cpp')
-rw-r--r--tests/TestCli.cpp756
1 files changed, 756 insertions, 0 deletions
diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp
new file mode 100644
index 000000000..02bc0ba3f
--- /dev/null
+++ b/tests/TestCli.cpp
@@ -0,0 +1,756 @@
+/*
+ * Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "TestCli.h"
+#include "config-keepassx-tests.h"
+#include "core/Global.h"
+#include "core/Config.h"
+#include "core/Bootstrap.h"
+#include "core/Tools.h"
+#include "core/PasswordGenerator.h"
+#include "crypto/Crypto.h"
+#include "format/KeePass2.h"
+#include "format/Kdbx3Reader.h"
+#include "format/Kdbx4Reader.h"
+#include "format/Kdbx4Writer.h"
+#include "format/Kdbx3Writer.h"
+#include "format/KdbxXmlReader.h"
+
+#include "cli/Command.h"
+#include "cli/Utils.h"
+#include "cli/Add.h"
+#include "cli/Clip.h"
+#include "cli/Diceware.h"
+#include "cli/Edit.h"
+#include "cli/Estimate.h"
+#include "cli/Extract.h"
+#include "cli/Generate.h"
+#include "cli/List.h"
+#include "cli/Locate.h"
+#include "cli/Merge.h"
+#include "cli/Remove.h"
+#include "cli/Show.h"
+
+#include <QFile>
+#include <QClipboard>
+#include <QFuture>
+#include <QtConcurrent>
+#include <QSet>
+
+#include <cstdio>
+
+QTEST_MAIN(TestCli)
+
+void TestCli::initTestCase()
+{
+ QVERIFY(Crypto::init());
+
+ Config::createTempFileInstance();
+ Bootstrap::bootstrapApplication();
+
+ // Load the NewDatabase.kdbx file into temporary storage
+ QFile sourceDbFile(QString(KEEPASSX_TEST_DATA_DIR).append("/NewDatabase.kdbx"));
+ QVERIFY(sourceDbFile.open(QIODevice::ReadOnly));
+ QVERIFY(Tools::readAllFromDevice(&sourceDbFile, m_dbData));
+ sourceDbFile.close();
+}
+
+void TestCli::init()
+{
+ m_dbFile.reset(new QTemporaryFile());
+ m_dbFile->open();
+ m_dbFile->write(m_dbData);
+ m_dbFile->flush();
+
+ m_stdinFile.reset(new QTemporaryFile());
+ m_stdinFile->open();
+ m_stdinHandle = fdopen(m_stdinFile->handle(), "r+");
+ Utils::STDIN = m_stdinHandle;
+
+ m_stdoutFile.reset(new QTemporaryFile());
+ m_stdoutFile->open();
+ m_stdoutHandle = fdopen(m_stdoutFile->handle(), "r+");
+ Utils::STDOUT = m_stdoutHandle;
+
+ m_stderrFile.reset(new QTemporaryFile());
+ m_stderrFile->open();
+ m_stderrHandle = fdopen(m_stderrFile->handle(), "r+");
+ Utils::STDERR = m_stderrHandle;
+}
+
+void TestCli::cleanup()
+{
+ m_dbFile.reset();
+
+ m_stdinFile.reset();
+ m_stdinHandle = stdin;
+ Utils::STDIN = stdin;
+
+ m_stdoutFile.reset();
+ Utils::STDOUT = stdout;
+ m_stdoutHandle = stdout;
+
+ m_stderrFile.reset();
+ m_stderrHandle = stderr;
+ Utils::STDERR = stderr;
+}
+
+void TestCli::cleanupTestCase()
+{
+}
+
+QSharedPointer<Database> TestCli::readTestDatabase() const
+{
+ Utils::setNextPassword("a");
+ auto db = QSharedPointer<Database>(Database::unlockFromStdin(m_dbFile->fileName(), "", m_stdoutHandle));
+ m_stdoutFile->seek(ftell(m_stdoutHandle)); // re-synchronize handles
+ return db;
+}
+
+void TestCli::testCommand()
+{
+ QCOMPARE(Command::getCommands().size(), 12);
+ QVERIFY(Command::getCommand("add"));
+ QVERIFY(Command::getCommand("clip"));
+ QVERIFY(Command::getCommand("diceware"));
+ QVERIFY(Command::getCommand("edit"));
+ QVERIFY(Command::getCommand("estimate"));
+ QVERIFY(Command::getCommand("extract"));
+ QVERIFY(Command::getCommand("generate"));
+ QVERIFY(Command::getCommand("locate"));
+ QVERIFY(Command::getCommand("ls"));
+ QVERIFY(Command::getCommand("merge"));
+ QVERIFY(Command::getCommand("rm"));
+ QVERIFY(Command::getCommand("show"));
+ QVERIFY(!Command::getCommand("doesnotexist"));
+}
+
+void TestCli::testAdd()
+{
+ Add addCmd;
+ QVERIFY(!addCmd.name.isEmpty());
+ QVERIFY(addCmd.getDescriptionLine().contains(addCmd.name));
+
+ Utils::setNextPassword("a");
+ addCmd.execute({"add", "-u", "newuser", "--url", "https://example.com/", "-g", "-l", "20", m_dbFile->fileName(), "/newuser-entry"});
+
+ auto db = readTestDatabase();
+ auto* entry = db->rootGroup()->findEntryByPath("/newuser-entry");
+ QVERIFY(entry);
+ QCOMPARE(entry->username(), QString("newuser"));
+ QCOMPARE(entry->url(), QString("https://example.com/"));
+ QCOMPARE(entry->password().size(), 20);
+
+ Utils::setNextPassword("a");
+ Utils::setNextPassword("newpassword");
+ addCmd.execute({"add", "-u", "newuser2", "--url", "https://example.net/", "-g", "-l", "20", "-p", m_dbFile->fileName(), "/newuser-entry2"});
+
+ db = readTestDatabase();
+ entry = db->rootGroup()->findEntryByPath("/newuser-entry2");
+ QVERIFY(entry);
+ QCOMPARE(entry->username(), QString("newuser2"));
+ QCOMPARE(entry->url(), QString("https://example.net/"));
+ QCOMPARE(entry->password(), QString("newpassword"));
+}
+
+void TestCli::testClip()
+{
+ QClipboard* clipboard = QGuiApplication::clipboard();
+ clipboard->clear();
+
+ Clip clipCmd;
+ QVERIFY(!clipCmd.name.isEmpty());
+ QVERIFY(clipCmd.getDescriptionLine().contains(clipCmd.name));
+
+ Utils::setNextPassword("a");
+ clipCmd.execute({"clip", m_dbFile->fileName(), "/Sample Entry"});
+
+ m_stderrFile->reset();
+ QString errorOutput(m_stderrFile->readAll());
+
+ if (errorOutput.contains("Unable to start program")
+ || errorOutput.contains("No program defined for clipboard manipulation")) {
+ QSKIP("Clip test skipped due to missing clipboard tool");
+ }
+
+ QCOMPARE(clipboard->text(), QString("Password"));
+
+ Utils::setNextPassword("a");
+ QFuture<void> future = QtConcurrent::run(&clipCmd, &Clip::execute, QStringList{"clip", m_dbFile->fileName(), "/Sample Entry", "1"});
+
+ QTRY_COMPARE_WITH_TIMEOUT(clipboard->text(), QString("Password"), 500);
+ QTRY_COMPARE_WITH_TIMEOUT(clipboard->text(), QString(""), 1500);
+
+ future.waitForFinished();
+}
+
+void TestCli::testDiceware()
+{
+ Diceware dicewareCmd;
+ QVERIFY(!dicewareCmd.name.isEmpty());
+ QVERIFY(dicewareCmd.getDescriptionLine().contains(dicewareCmd.name));
+
+ dicewareCmd.execute({"diceware"});
+ m_stdoutFile->reset();
+ QString passphrase(m_stdoutFile->readLine());
+ QVERIFY(!passphrase.isEmpty());
+
+ dicewareCmd.execute({"diceware", "-W", "2"});
+ m_stdoutFile->seek(passphrase.toLatin1().size());
+ passphrase = m_stdoutFile->readLine();
+ QCOMPARE(passphrase.split(" ").size(), 2);
+
+ auto pos = m_stdoutFile->pos();
+ dicewareCmd.execute({"diceware", "-W", "10"});
+ m_stdoutFile->seek(pos);
+ passphrase = m_stdoutFile->readLine();
+ QCOMPARE(passphrase.split(" ").size(), 10);
+
+ QTemporaryFile wordFile;
+ wordFile.open();
+ for (int i = 0; i < 4500; ++i) {
+ wordFile.write(QString("word" + QString::number(i) + "\n").toLatin1());
+ }
+ wordFile.close();
+
+ pos = m_stdoutFile->pos();
+ dicewareCmd.execute({"diceware", "-W", "11", "-w", wordFile.fileName()});
+ m_stdoutFile->seek(pos);
+ passphrase = m_stdoutFile->readLine();
+ const auto words = passphrase.split(" ");
+ QCOMPARE(words.size(), 11);
+ QRegularExpression regex("^word\\d+$");
+ for (const auto& word: words) {
+ QVERIFY2(regex.match(word).hasMatch(), qPrintable("Word " + word + " was not on the word list"));
+ }
+}
+
+void TestCli::testEdit()
+{
+ Edit editCmd;
+ QVERIFY(!editCmd.name.isEmpty());
+ QVERIFY(editCmd.getDescriptionLine().contains(editCmd.name));
+
+ Utils::setNextPassword("a");
+ editCmd.execute({"edit", "-u", "newuser", "--url", "https://otherurl.example.com/", "-t", "newtitle", m_dbFile->fileName(), "/Sample Entry"});
+
+ auto db = readTestDatabase();
+ auto* entry = db->rootGroup()->findEntryByPath("/newtitle");
+ QVERIFY(entry);
+ QCOMPARE(entry->username(), QString("newuser"));
+ QCOMPARE(entry->url(), QString("https://otherurl.example.com/"));
+ QCOMPARE(entry->password(), QString("Password"));
+
+ Utils::setNextPassword("a");
+ editCmd.execute({"edit", "-g", m_dbFile->fileName(), "/newtitle"});
+ db = readTestDatabase();
+ entry = db->rootGroup()->findEntryByPath("/newtitle");
+ QVERIFY(entry);
+ QCOMPARE(entry->username(), QString("newuser"));
+ QCOMPARE(entry->url(), QString("https://otherurl.example.com/"));
+ QVERIFY(!entry->password().isEmpty());
+ QVERIFY(entry->password() != QString("Password"));
+
+ Utils::setNextPassword("a");
+ editCmd.execute({"edit", "-g", "-l", "34", "-t", "yet another title", m_dbFile->fileName(), "/newtitle"});
+ db = readTestDatabase();
+ entry = db->rootGroup()->findEntryByPath("/yet another title");
+ QVERIFY(entry);
+ QCOMPARE(entry->username(), QString("newuser"));
+ QCOMPARE(entry->url(), QString("https://otherurl.example.com/"));
+ QVERIFY(entry->password() != QString("Password"));
+ QCOMPARE(entry->password().size(), 34);
+
+ Utils::setNextPassword("a");
+ Utils::setNextPassword("newpassword");
+ editCmd.execute({"edit", "-p", m_dbFile->fileName(), "/yet another title"});
+ db = readTestDatabase();
+ entry = db->rootGroup()->findEntryByPath("/yet another title");
+ QVERIFY(entry);
+ QCOMPARE(entry->password(), QString("newpassword"));
+}
+
+void TestCli::testEstimate_data()
+{
+ QTest::addColumn<QString>("input");
+ QTest::addColumn<QString>("length");
+ QTest::addColumn<QString>("entropy");
+ QTest::addColumn<QString>("log10");
+ QTest::addColumn<QStringList>("searchStrings");
+
+ QTest::newRow("Dictionary")
+ << "password" << "8" << "1.0" << "0.3"
+ << QStringList{"Type: Dictionary", "\tpassword"};
+
+ QTest::newRow("Spatial")
+ << "zxcv" << "4" << "10.3" << "3.1"
+ << QStringList{"Type: Spatial", "\tzxcv"};
+
+ QTest::newRow("Spatial(Rep)")
+ << "sdfgsdfg" << "8" << "11.3" << "3.4"
+ << QStringList{"Type: Spatial(Rep)", "\tsdfgsdfg"};
+
+ QTest::newRow("Dictionary / Sequence")
+ << "password123" << "11" << "4.5" << "1.3"
+ << QStringList{"Type: Dictionary", "Type: Sequence", "\tpassword", "\t123"};
+
+ QTest::newRow("Dict+Leet")
+ << "p455w0rd" << "8" << "2.5" << "0.7"
+ << QStringList{"Type: Dict+Leet", "\tp455w0rd"};
+
+ QTest::newRow("Dictionary(Rep)")
+ << "hellohello" << "10" << "7.3" << "2.2"
+ << QStringList{"Type: Dictionary(Rep)", "\thellohello"};
+
+ QTest::newRow("Sequence(Rep) / Dictionary")
+ << "456456foobar" << "12" << "16.7" << "5.0"
+ << QStringList{"Type: Sequence(Rep)", "Type: Dictionary", "\t456456", "\tfoobar"};
+
+ QTest::newRow("Bruteforce(Rep) / Bruteforce")
+ << "xzxzy" << "5" << "16.1" << "4.8"
+ << QStringList{"Type: Bruteforce(Rep)", "Type: Bruteforce", "\txzxz", "\ty"};
+
+ QTest::newRow("Dictionary / Date(Rep)")
+ << "pass20182018" << "12" << "15.1" << "4.56"
+ << QStringList{"Type: Dictionary", "Type: Date(Rep)", "\tpass", "\t20182018"};
+
+ QTest::newRow("Dictionary / Date / Bruteforce")
+ << "mypass2018-2" << "12" << "32.9" << "9.9"
+ << QStringList{"Type: Dictionary", "Type: Date", "Type: Bruteforce", "\tmypass", "\t2018", "\t-2"};
+
+ QTest::newRow("Strong Password")
+ << "E*!%.Qw{t.X,&bafw)\"Q!ah$%;U/" << "28" << "165.7" << "49.8"
+ << QStringList{"Type: Bruteforce", "\tE*"};
+
+ // TODO: detect passphrases and adjust entropy calculation accordingly (issue #2347)
+ QTest::newRow("Strong Passphrase")
+ << "squint wooing resupply dangle isolation axis headsman" << "53" << "151.2" << "45.5"
+ << QStringList{"Type: Dictionary", "Type: Bruteforce", "Multi-word extra bits 22.0", "\tsquint", "\t ", "\twooing"};
+}
+
+void TestCli::testEstimate()
+{
+ QFETCH(QString, input);
+ QFETCH(QString, length);
+ QFETCH(QString, entropy);
+ QFETCH(QString, log10);
+ QFETCH(QStringList, searchStrings);
+
+ Estimate estimateCmd;
+ QVERIFY(!estimateCmd.name.isEmpty());
+ QVERIFY(estimateCmd.getDescriptionLine().contains(estimateCmd.name));
+
+ QTextStream in(m_stdinFile.data());
+ QTextStream out(m_stdoutFile.data());
+
+ in << input << endl;
+ auto inEnd = in.pos();
+ in.seek(0);
+ estimateCmd.execute({"estimate"});
+ auto outEnd = out.pos();
+ out.seek(0);
+ auto result = out.readAll();
+ QVERIFY(result.startsWith("Length " + length));
+ QVERIFY(result.contains("Entropy " + entropy));
+ QVERIFY(result.contains("Log10 " + log10));
+
+ // seek to end of stream
+ in.seek(inEnd);
+ out.seek(outEnd);
+
+ in << input << endl;
+ in.seek(inEnd);
+ estimateCmd.execute({"estimate", "-a"});
+ out.seek(outEnd);
+ result = out.readAll();
+ QVERIFY(result.startsWith("Length " + length));
+ QVERIFY(result.contains("Entropy " + entropy));
+ QVERIFY(result.contains("Log10 " + log10));
+ for (const auto& string: asConst(searchStrings)) {
+ QVERIFY2(result.contains(string), qPrintable("String " + string + " missing"));
+ }
+}
+
+void TestCli::testExtract()
+{
+ Extract extractCmd;
+ QVERIFY(!extractCmd.name.isEmpty());
+ QVERIFY(extractCmd.getDescriptionLine().contains(extractCmd.name));
+
+ Utils::setNextPassword("a");
+ extractCmd.execute({"extract", m_dbFile->fileName()});
+
+ m_stdoutFile->seek(0);
+ m_stdoutFile->readLine(); // skip prompt line
+
+ KdbxXmlReader reader(KeePass2::FILE_VERSION_3_1);
+ QScopedPointer<Database> db(new Database());
+ reader.readDatabase(m_stdoutFile.data(), db.data());
+ QVERIFY(!reader.hasError());
+ QVERIFY(db.data());
+ auto* entry = db->rootGroup()->findEntryByPath("/Sample Entry");
+ QVERIFY(entry);
+ QCOMPARE(entry->password(), QString("Password"));
+}
+
+void TestCli::testGenerate_data()
+{
+ QTest::addColumn<QStringList>("parameters");
+ QTest::addColumn<QString>("pattern");
+
+ QTest::newRow("default") << QStringList{"generate"} << "^[^\r\n]+$";
+ QTest::newRow("length") << QStringList{"generate", "-L", "13"} << "^.{13}$";
+ QTest::newRow("lowercase") << QStringList{"generate", "-L", "14", "-l"} << "^[a-z]{14}$";
+ QTest::newRow("uppercase") << QStringList{"generate", "-L", "15", "-u"} << "^[A-Z]{15}$";
+ QTest::newRow("numbers")<< QStringList{"generate", "-L", "16", "-n"} << "^[0-9]{16}$";
+ QTest::newRow("special")
+ << QStringList{"generate", "-L", "200", "-s"}
+ << R"(^[\(\)\[\]\{\}\.\-*|\\,:;"'\/\_!+-<=>?#$%&^`@~]{200}$)";
+ QTest::newRow("special (exclude)")
+ << QStringList{"generate", "-L", "200", "-s" , "-x", "+.?@&"}
+ << R"(^[\(\)\[\]\{\}\.\-*|\\,:;"'\/\_!-<=>#$%^`~]{200}$)";
+ QTest::newRow("extended")
+ << QStringList{"generate", "-L", "50", "-e"}
+ << R"(^[^a-zA-Z0-9\(\)\[\]\{\}\.\-\*\|\\,:;"'\/\_!+-<=>?#$%&^`@~]{50}$)";
+ QTest::newRow("numbers + lowercase + uppercase")
+ << QStringList{"generate", "-L", "16", "-n", "-u", "-l"}
+ << "^[0-9a-zA-Z]{16}$";
+ QTest::newRow("numbers + lowercase + uppercase (exclude)")
+ << QStringList{"generate", "-L", "500", "-n", "-u", "-l", "-x", "abcdefg0123@"}
+ << "^[^abcdefg0123@]{500}$";
+ QTest::newRow("numbers + lowercase + uppercase (exclude similar)")
+ << QStringList{"generate", "-L", "200", "-n", "-u", "-l", "--exclude-similar"}
+ << "^[^l1IO0]{200}$";
+ QTest::newRow("uppercase + lowercase (every)")
+ << QStringList{"generate", "-L", "2", "-u", "-l", "--every-group"}
+ << "^[a-z][A-Z]|[A-Z][a-z]$";
+ QTest::newRow("numbers + lowercase (every)")
+ << QStringList{"generate", "-L", "2", "-n", "-l", "--every-group"}
+ << "^[a-z][0-9]|[0-9][a-z]$";
+}
+
+void TestCli::testGenerate()
+{
+ QFETCH(QStringList, parameters);
+ QFETCH(QString, pattern);
+
+ Generate generateCmd;
+ QVERIFY(!generateCmd.name.isEmpty());
+ QVERIFY(generateCmd.getDescriptionLine().contains(generateCmd.name));
+
+ qint64 pos = 0;
+ // run multiple times to make accidental passes unlikely
+ for (int i = 0; i < 10; ++i) {
+ generateCmd.execute(parameters);
+ m_stdoutFile->seek(pos);
+ QRegularExpression regex(pattern);
+ QString password = QString::fromUtf8(m_stdoutFile->readLine());
+ pos = m_stdoutFile->pos();
+ QVERIFY2(regex.match(password).hasMatch(), qPrintable("Password " + password + " does not match pattern " + pattern));
+ }
+}
+
+void TestCli::testList()
+{
+ List listCmd;
+ QVERIFY(!listCmd.name.isEmpty());
+ QVERIFY(listCmd.getDescriptionLine().contains(listCmd.name));
+
+ Utils::setNextPassword("a");
+ listCmd.execute({"ls", m_dbFile->fileName()});
+ m_stdoutFile->reset();
+ m_stdoutFile->readLine(); // skip password prompt
+ QCOMPARE(m_stdoutFile->readAll(), QByteArray("Sample Entry\n"
+ "General/\n"
+ "Windows/\n"
+ "Network/\n"
+ "Internet/\n"
+ "eMail/\n"
+ "Homebanking/\n"));
+
+ qint64 pos = m_stdoutFile->pos();
+ Utils::setNextPassword("a");
+ listCmd.execute({"ls", "-R", m_dbFile->fileName()});
+ m_stdoutFile->seek(pos);
+ m_stdoutFile->readLine(); // skip password prompt
+ QCOMPARE(m_stdoutFile->readAll(), QByteArray("Sample Entry\n"
+ "General/\n"
+ " [empty]\n"
+ "Windows/\n"
+ " [empty]\n"
+ "Network/\n"
+ " [empty]\n"
+ "Internet/\n"
+ " [empty]\n"
+ "eMail/\n"
+ " [empty]\n"
+ "Homebanking/\n"
+ " [empty]\n"));
+
+ pos = m_stdoutFile->pos();
+ Utils::setNextPassword("a");
+ listCmd.execute({"ls", m_dbFile->fileName(), "/General/"});
+ m_stdoutFile->seek(pos);
+ m_stdoutFile->readLine();
+ QCOMPARE(m_stdoutFile->readAll(), QByteArray("[empty]\n"));
+
+ pos = m_stdoutFile->pos();
+ Utils::setNextPassword("a");
+ listCmd.execute({"ls", m_dbFile->fileName(), "/DoesNotExist/"});
+ m_stdoutFile->seek(pos);
+ m_stdoutFile->readLine(); // skip password prompt
+ m_stderrFile->reset();
+ QCOMPARE(m_stdoutFile->readAll(), QByteArray(""));
+ QCOMPARE(m_stderrFile->readAll(), QByteArray("Cannot find group /DoesNotExist/.\n"));
+}
+
+void TestCli::testLocate()
+{
+ Locate locateCmd;
+ QVERIFY(!locateCmd.name.isEmpty());
+ QVERIFY(locateCmd.getDescriptionLine().contains(locateCmd.name));
+
+ Utils::setNextPassword("a");
+ locateCmd.execute({"locate", m_dbFile->fileName(), "Sample"});
+ m_stdoutFile->reset();
+ m_stdoutFile->readLine(); // skip password prompt
+ QCOMPARE(m_stdoutFile->readAll(), QByteArray("/Sample Entry\n"));
+
+ qint64 pos = m_stdoutFile->pos();
+ Utils::setNextPassword("a");
+ locateCmd.execute({"locate", m_dbFile->fileName(), "Does Not Exist"});
+ m_stdoutFile->seek(pos);
+ m_stdoutFile->readLine(); // skip password prompt
+ m_stderrFile->reset();
+ QCOMPARE(m_stdoutFile->readAll(), QByteArray(""));
+ QCOMPARE(m_stderrFile->readAll(), QByteArray("No results for that search term.\n"));
+
+ // write a modified database
+ auto db = readTestDatabase();
+ QVERIFY(db);
+ auto* group = db->rootGroup()->findGroupByPath("/General/");
+ QVERIFY(group);
+ auto* entry = new Entry();
+ entry->setUuid(QUuid::createUuid());
+ entry->setTitle("New Entry");
+ group->addEntry(entry);
+ QTemporaryFile tmpFile;
+ tmpFile.open();
+ Kdbx4Writer writer;
+ writer.writeDatabase(&tmpFile, db.data());
+ tmpFile.close();
+
+ pos = m_stdoutFile->pos();
+ Utils::setNextPassword("a");
+ locateCmd.execute({"locate", tmpFile.fileName(), "New"});
+ m_stdoutFile->seek(pos);
+ m_stdoutFile->readLine(); // skip password prompt
+ QCOMPARE(m_stdoutFile->readAll(), QByteArray("/General/New Entry\n"));
+
+ pos = m_stdoutFile->pos();
+ Utils::setNextPassword("a");
+ locateCmd.execute({"locate", tmpFile.fileName(), "Entry"});
+ m_stdoutFile->seek(pos);
+ m_stdoutFile->readLine(); // skip password prompt
+ QCOMPARE(m_stdoutFile->readAll(), QByteArray("/Sample Entry\n/General/New Entry\n"));
+}
+
+void TestCli::testMerge()
+{
+ Merge mergeCmd;
+ QVERIFY(!mergeCmd.name.isEmpty());
+ QVERIFY(mergeCmd.getDescriptionLine().contains(mergeCmd.name));
+
+ Kdbx4Writer writer;
+ Kdbx4Reader reader;
+
+ // load test database and save a copy
+ auto db = readTestDatabase();
+ QVERIFY(db);
+ QTemporaryFile targetFile1;
+ targetFile1.open();
+ writer.writeDatabase(&targetFile1, db.data());
+ targetFile1.close();
+
+ // save another copy with a different password
+ QTemporaryFile targetFile2;
+ targetFile2.open();
+ auto oldKey = db->key();
+ auto key = QSharedPointer<CompositeKey>::create();
+ key->addKey(QSharedPointer<PasswordKey>::create("b"));
+ db->setKey(key);
+ writer.writeDatabase(&targetFile2, db.data());
+ targetFile2.close();
+ db->setKey(oldKey);
+
+ // then add a new entry to the in-memory database and save another copy
+ auto* entry = new Entry();
+ entry->setUuid(QUuid::createUuid());
+ entry->setTitle("Some Website");
+ entry->setPassword("secretsecretsecret");
+ auto* group = db->rootGroup()->findGroupByPath("/Internet/");
+ QVERIFY(group);
+ group->addEntry(entry);
+ QTemporaryFile sourceFile;
+ sourceFile.open();
+ writer.writeDatabase(&sourceFile, db.data());
+ sourceFile.close();
+
+ qint64 pos = m_stdoutFile->pos();
+ Utils::setNextPassword("a");
+ mergeCmd.execute({"merge", "-s", targetFile1.fileName(), sourceFile.fileName()});
+ m_stdoutFile->seek(pos);
+ m_stdoutFile->readLine();
+ QCOMPARE(m_stdoutFile->readAll(), QByteArray("Successfully merged the database files.\n"));
+
+ QFile readBack(targetFile1.fileName());
+ readBack.open(QIODevice::ReadOnly);
+ QScopedPointer<Database> mergedDb(reader.readDatabase(&readBack, oldKey));
+ readBack.close();
+ QVERIFY(mergedDb);
+ auto* entry1 = mergedDb->rootGroup()->findEntryByPath("/Internet/Some Website");
+ QVERIFY(entry1);
+ QCOMPARE(entry1->title(), QString("Some Website"));
+ QCOMPARE(entry1->password(), QString("secretsecretsecret"));
+
+ // try again with different passwords for both files
+ pos = m_stdoutFile->pos();
+ Utils::setNextPassword("b");
+ Utils::setNextPassword("a");
+ mergeCmd.execute({"merge", targetFile2.fileName(), sourceFile.fileName()});
+ m_stdoutFile->seek(pos);
+ m_stdoutFile->readLine();
+ m_stdoutFile->readLine();
+ QCOMPARE(m_stdoutFile->readAll(), QByteArray("Successfully merged the database files.\n"));
+
+ readBack.setFileName(targetFile2.fileName());
+ readBack.open(QIODevice::ReadOnly);
+ mergedDb.reset(reader.readDatabase(&readBack, key));
+ readBack.close();
+ QVERIFY(mergedDb);
+ entry1 = mergedDb->rootGroup()->findEntryByPath("/Internet/Some Website");
+ QVERIFY(entry1);
+ QCOMPARE(entry1->title(), QString("Some Website"));
+ QCOMPARE(entry1->password(), QString("secretsecretsecret"));
+}
+
+void TestCli::testRemove()
+{
+ Remove removeCmd;
+ QVERIFY(!removeCmd.name.isEmpty());
+ QVERIFY(removeCmd.getDescriptionLine().contains(removeCmd.name));
+
+ Kdbx3Reader reader;
+ Kdbx3Writer writer;
+
+ // load test database and save a copy with disabled recycle bin
+ auto db = readTestDatabase();
+ QVERIFY(db);
+ QTemporaryFile fileCopy;
+ fileCopy.open();
+ db->metadata()->setRecycleBinEnabled(false);
+ writer.writeDatabase(&fileCopy, db.data());
+ fileCopy.close();
+
+ qint64 pos = m_stdoutFile->pos();
+
+ // delete entry and verify
+ Utils::setNextPassword("a");
+ removeCmd.execute({"rm", m_dbFile->fileName(), "/Sample Entry"});
+ m_stdoutFile->seek(pos);
+ m_stdoutFile->readLine(); // skip password prompt
+ QCOMPARE(m_stdoutFile->readAll(), QByteArray("Successfully recycled entry Sample Entry.\n"));
+
+ auto key = QSharedPointer<CompositeKey>::create();
+ key->addKey(QSharedPointer<PasswordKey>::create("a"));
+ QFile readBack(m_dbFile->fileName());
+ readBack.open(QIODevice::ReadOnly);
+ QScopedPointer<Database> readBackDb(reader.readDatabase(&readBack, key));
+ readBack.close();
+ QVERIFY(readBackDb);
+ QVERIFY(!readBackDb->rootGroup()->findEntryByPath("/Sample Entry"));
+ QVERIFY(readBackDb->rootGroup()->findEntryByPath("/Recycle Bin/Sample Entry"));
+
+ pos = m_stdoutFile->pos();
+
+ // try again, this time without recycle bin
+ Utils::setNextPassword("a");
+ removeCmd.execute({"rm", fileCopy.fileName(), "/Sample Entry"});
+ m_stdoutFile->seek(pos);
+ m_stdoutFile->readLine(); // skip password prompt
+ QCOMPARE(m_stdoutFile->readAll(), QByteArray("Successfully deleted entry Sample Entry.\n"));
+
+ readBack.setFileName(fileCopy.fileName());
+ readBack.open(QIODevice::ReadOnly);
+ readBackDb.reset(reader.readDatabase(&readBack, key));
+ readBack.close();
+ QVERIFY(readBackDb);
+ QVERIFY(!readBackDb->rootGroup()->findEntryByPath("/Sample Entry"));
+ QVERIFY(!readBackDb->rootGroup()->findEntryByPath("/Recycle Bin/Sample Entry"));
+
+ pos = m_stdoutFile->pos();
+
+ // finally, try deleting a non-existent entry
+ Utils::setNextPassword("a");
+ removeCmd.execute({"rm", fileCopy.fileName(), "/Sample Entry"});
+ m_stdoutFile->seek(pos);
+ m_stdoutFile->readLine(); // skip password prompt
+ m_stderrFile->reset();
+ QCOMPARE(m_stdoutFile->readAll(), QByteArray(""));
+ QCOMPARE(m_stderrFile->readAll(), QByteArray("Entry /Sample Entry not found.\n"));
+}
+
+void TestCli::testShow()
+{
+ Show showCmd;
+ QVERIFY(!showCmd.name.isEmpty());
+ QVERIFY(showCmd.getDescriptionLine().contains(showCmd.name));
+
+ Utils::setNextPassword("a");
+ showCmd.execute({"show", m_dbFile->fileName(), "/Sample Entry"});
+ m_stdoutFile->reset();
+ m_stdoutFile->readLine(); // skip password prompt
+ QCOMPARE(m_stdoutFile->readAll(), QByteArray("Title: Sample Entry\n"
+ "UserName: User Name\n"
+ "Password: Password\n"
+ "URL: http://www.somesite.com/\n"
+ "Notes: Notes\n"));
+
+ qint64 pos = m_stdoutFile->pos();
+ Utils::setNextPassword("a");
+ showCmd.execute({"show", "-a", "Title", m_dbFile->fileName(), "/Sample Entry"});
+ m_stdoutFile->seek(pos);
+ m_stdoutFile->readLine(); // skip password prompt
+ QCOMPARE(m_stdoutFile->readAll(), QByteArray("Sample Entry\n"));
+
+ pos = m_stdoutFile->pos();
+ Utils::setNextPassword("a");
+ showCmd.execute({"show", "-a", "Title", "-a", "URL", m_dbFile->fileName(), "/Sample Entry"});
+ m_stdoutFile->seek(pos);
+ m_stdoutFile->readLine(); // skip password prompt
+ QCOMPARE(m_stdoutFile->readAll(), QByteArray("Sample Entry\n"
+ "http://www.somesite.com/\n"));
+
+ pos = m_stdoutFile->pos();
+ Utils::setNextPassword("a");
+ showCmd.execute({"show", "-a", "DoesNotExist", m_dbFile->fileName(), "/Sample Entry"});
+ m_stdoutFile->seek(pos);
+ m_stdoutFile->readLine(); // skip password prompt
+ m_stderrFile->reset();
+ QCOMPARE(m_stdoutFile->readAll(), QByteArray(""));
+ QCOMPARE(m_stderrFile->readAll(), QByteArray("ERROR: unknown attribute DoesNotExist.\n"));
+}