diff --git a/src/cli/Add.cpp b/src/cli/Add.cpp index 257ea7f6b..68240adb6 100644 --- a/src/cli/Add.cpp +++ b/src/cli/Add.cpp @@ -48,6 +48,7 @@ int Add::execute(const QStringList& arguments) QCommandLineParser parser; parser.setApplicationDescription(description); parser.addPositionalArgument("database", QObject::tr("Path of the database.")); + parser.addOption(Command::QuietOption); QCommandLineOption keyFile(QStringList() << "k" << "key-file", QObject::tr("Key file of the database."), @@ -89,7 +90,10 @@ int Add::execute(const QStringList& arguments) const QString& databasePath = args.at(0); const QString& entryPath = args.at(1); - auto db = Database::unlockFromStdin(databasePath, parser.value(keyFile), Utils::STDOUT, Utils::STDERR); + auto db = Database::unlockFromStdin(databasePath, + parser.value(keyFile), + parser.isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT, + Utils::STDERR); if (!db) { return EXIT_FAILURE; } @@ -117,8 +121,10 @@ int Add::execute(const QStringList& arguments) } if (parser.isSet(prompt)) { - outputTextStream << QObject::tr("Enter password for new entry: ") << flush; - QString password = Utils::getPassword(); + if (!parser.isSet(Command::QuietOption)) { + outputTextStream << QObject::tr("Enter password for new entry: ") << flush; + } + QString password = Utils::getPassword(parser.isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT); entry->setPassword(password); } else if (parser.isSet(generate)) { PasswordGenerator passwordGenerator; @@ -141,6 +147,8 @@ int Add::execute(const QStringList& arguments) return EXIT_FAILURE; } - outputTextStream << QObject::tr("Successfully added entry %1.").arg(entry->title()) << endl; + if (!parser.isSet(Command::QuietOption)) { + outputTextStream << QObject::tr("Successfully added entry %1.").arg(entry->title()) << endl; + } return EXIT_SUCCESS; } diff --git a/src/cli/Clip.cpp b/src/cli/Clip.cpp index 6b466c5a7..224841f60 100644 --- a/src/cli/Clip.cpp +++ b/src/cli/Clip.cpp @@ -51,6 +51,7 @@ int Clip::execute(const QStringList& arguments) QObject::tr("Key file of the database."), QObject::tr("path")); parser.addOption(keyFile); + parser.addOption(Command::QuietOption); QCommandLineOption totp(QStringList() << "t" << "totp", QObject::tr("Copy the current TOTP to the clipboard.")); parser.addOption(totp); @@ -66,15 +67,22 @@ int Clip::execute(const QStringList& arguments) return EXIT_FAILURE; } - auto db = Database::unlockFromStdin(args.at(0), parser.value(keyFile), Utils::STDOUT, Utils::STDERR); + auto db = Database::unlockFromStdin(args.at(0), + parser.value(keyFile), + parser.isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT, + Utils::STDERR); if (!db) { return EXIT_FAILURE; } - return clipEntry(db, args.at(1), args.value(2), parser.isSet(totp)); + return clipEntry(db, args.at(1), args.value(2), parser.isSet(totp), parser.isSet(Command::QuietOption)); } -int Clip::clipEntry(QSharedPointer database, const QString& entryPath, const QString& timeout, bool clipTotp) +int Clip::clipEntry(QSharedPointer database, + const QString& entryPath, + const QString& timeout, + bool clipTotp, + bool silent) { TextStream err(Utils::STDERR); @@ -86,7 +94,7 @@ int Clip::clipEntry(QSharedPointer database, const QString& entryPath, timeoutSeconds = timeout.toInt(); } - TextStream outputTextStream(Utils::STDOUT, QIODevice::WriteOnly); + TextStream outputTextStream(silent ? Utils::DEVNULL : Utils::STDOUT, QIODevice::WriteOnly); Entry* entry = database->rootGroup()->findEntryByPath(entryPath); if (!entry) { err << QObject::tr("Entry %1 not found.").arg(entryPath) << endl; diff --git a/src/cli/Clip.h b/src/cli/Clip.h index 87f56bb0e..bdc1de03d 100644 --- a/src/cli/Clip.h +++ b/src/cli/Clip.h @@ -26,7 +26,11 @@ public: Clip(); ~Clip(); int execute(const QStringList& arguments) override; - int clipEntry(QSharedPointer database, const QString& entryPath, const QString& timeout, bool clipTotp); + int clipEntry(QSharedPointer database, + const QString& entryPath, + const QString& timeout, + bool clipTotp, + bool silent); }; #endif // KEEPASSXC_CLIP_H diff --git a/src/cli/Command.cpp b/src/cli/Command.cpp index a95676ff0..a93452bcb 100644 --- a/src/cli/Command.cpp +++ b/src/cli/Command.cpp @@ -35,6 +35,10 @@ #include "Remove.h" #include "Show.h" +const QCommandLineOption Command::QuietOption = + QCommandLineOption(QStringList() << "q" + << "quiet", + QObject::tr("Silence password prompt and other secondary outputs.")); QMap commands; Command::~Command() diff --git a/src/cli/Command.h b/src/cli/Command.h index 4e4e076de..beb9d4087 100644 --- a/src/cli/Command.h +++ b/src/cli/Command.h @@ -18,6 +18,7 @@ #ifndef KEEPASSXC_COMMAND_H #define KEEPASSXC_COMMAND_H +#include #include #include #include @@ -36,6 +37,8 @@ public: static QList getCommands(); static Command* getCommand(const QString& commandName); + + static const QCommandLineOption QuietOption; }; #endif // KEEPASSXC_COMMAND_H diff --git a/src/cli/Diceware.cpp b/src/cli/Diceware.cpp index d2821e7cc..bb7931352 100644 --- a/src/cli/Diceware.cpp +++ b/src/cli/Diceware.cpp @@ -22,9 +22,9 @@ #include +#include "Utils.h" #include "cli/TextStream.h" #include "core/PassphraseGenerator.h" -#include "Utils.h" Diceware::Diceware() { diff --git a/src/cli/Edit.cpp b/src/cli/Edit.cpp index cb7c58baa..bbb1473c8 100644 --- a/src/cli/Edit.cpp +++ b/src/cli/Edit.cpp @@ -48,6 +48,7 @@ int Edit::execute(const QStringList& arguments) QCommandLineParser parser; parser.setApplicationDescription(description); parser.addPositionalArgument("database", QObject::tr("Path of the database.")); + parser.addOption(Command::QuietOption); QCommandLineOption keyFile(QStringList() << "k" << "key-file", QObject::tr("Key file of the database."), @@ -93,7 +94,10 @@ int Edit::execute(const QStringList& arguments) const QString& databasePath = args.at(0); const QString& entryPath = args.at(1); - auto db = Database::unlockFromStdin(databasePath, parser.value(keyFile), Utils::STDOUT, Utils::STDERR); + auto db = Database::unlockFromStdin(databasePath, + parser.value(keyFile), + parser.isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT, + Utils::STDERR); if (!db) { return EXIT_FAILURE; } @@ -132,8 +136,10 @@ int Edit::execute(const QStringList& arguments) } if (parser.isSet(prompt)) { - out << QObject::tr("Enter new password for entry: ") << flush; - QString password = Utils::getPassword(); + if (!parser.isSet(Command::QuietOption)) { + out << QObject::tr("Enter new password for entry: ") << flush; + } + QString password = Utils::getPassword(parser.isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT); entry->setPassword(password); } else if (parser.isSet(generate)) { PasswordGenerator passwordGenerator; @@ -158,6 +164,8 @@ int Edit::execute(const QStringList& arguments) return EXIT_FAILURE; } - out << QObject::tr("Successfully edited entry %1.").arg(entry->title()) << endl; + if (!parser.isSet(Command::QuietOption)) { + out << QObject::tr("Successfully edited entry %1.").arg(entry->title()) << endl; + } return EXIT_SUCCESS; } diff --git a/src/cli/Extract.cpp b/src/cli/Extract.cpp index 01b3d1ebe..e6afdfbdb 100644 --- a/src/cli/Extract.cpp +++ b/src/cli/Extract.cpp @@ -49,6 +49,7 @@ int Extract::execute(const QStringList& arguments) QCommandLineParser parser; parser.setApplicationDescription(description); parser.addPositionalArgument("database", QObject::tr("Path of the database to extract.")); + parser.addOption(Command::QuietOption); QCommandLineOption keyFile(QStringList() << "k" << "key-file", QObject::tr("Key file of the database."), QObject::tr("path")); @@ -62,11 +63,13 @@ int Extract::execute(const QStringList& arguments) return EXIT_FAILURE; } - out << QObject::tr("Insert password to unlock %1: ").arg(args.at(0)) << flush; + if (!parser.isSet(Command::QuietOption)) { + out << QObject::tr("Insert password to unlock %1: ").arg(args.at(0)) << flush; + } auto compositeKey = QSharedPointer::create(); - QString line = Utils::getPassword(); + QString line = Utils::getPassword(parser.isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT); auto passwordKey = QSharedPointer::create(); passwordKey->setPassword(line); compositeKey->addKey(passwordKey); diff --git a/src/cli/List.cpp b/src/cli/List.cpp index 3b8938189..ca96799f0 100644 --- a/src/cli/List.cpp +++ b/src/cli/List.cpp @@ -46,6 +46,7 @@ int List::execute(const QStringList& arguments) parser.setApplicationDescription(description); parser.addPositionalArgument("database", QObject::tr("Path of the database.")); parser.addPositionalArgument("group", QObject::tr("Path of the group to list. Default is /"), "[group]"); + parser.addOption(Command::QuietOption); QCommandLineOption keyFile(QStringList() << "k" << "key-file", QObject::tr("Key file of the database."), QObject::tr("path")); @@ -64,7 +65,10 @@ int List::execute(const QStringList& arguments) bool recursive = parser.isSet(recursiveOption); - auto db = Database::unlockFromStdin(args.at(0), parser.value(keyFile), Utils::STDOUT, Utils::STDERR); + auto db = Database::unlockFromStdin(args.at(0), + parser.value(keyFile), + parser.isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT, + Utils::STDERR); if (!db) { return EXIT_FAILURE; } diff --git a/src/cli/Locate.cpp b/src/cli/Locate.cpp index 8039f0693..ab45115db 100644 --- a/src/cli/Locate.cpp +++ b/src/cli/Locate.cpp @@ -25,9 +25,9 @@ #include "cli/TextStream.h" #include "cli/Utils.h" -#include "core/Global.h" #include "core/Database.h" #include "core/Entry.h" +#include "core/Global.h" #include "core/Group.h" Locate::Locate() @@ -48,6 +48,7 @@ int Locate::execute(const QStringList& arguments) parser.setApplicationDescription(description); parser.addPositionalArgument("database", QObject::tr("Path of the database.")); parser.addPositionalArgument("term", QObject::tr("Search term.")); + parser.addOption(Command::QuietOption); QCommandLineOption keyFile(QStringList() << "k" << "key-file", QObject::tr("Key file of the database."), QObject::tr("path")); @@ -61,7 +62,10 @@ int Locate::execute(const QStringList& arguments) return EXIT_FAILURE; } - auto db = Database::unlockFromStdin(args.at(0), parser.value(keyFile), Utils::STDOUT, Utils::STDERR); + auto db = Database::unlockFromStdin(args.at(0), + parser.value(keyFile), + parser.isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT, + Utils::STDERR); if (!db) { return EXIT_FAILURE; } diff --git a/src/cli/Merge.cpp b/src/cli/Merge.cpp index e5b01665c..b17d27f7e 100644 --- a/src/cli/Merge.cpp +++ b/src/cli/Merge.cpp @@ -20,9 +20,9 @@ #include #include "cli/TextStream.h" +#include "cli/Utils.h" #include "core/Database.h" #include "core/Merger.h" -#include "cli/Utils.h" #include @@ -45,10 +45,11 @@ int Merge::execute(const QStringList& arguments) parser.setApplicationDescription(description); parser.addPositionalArgument("database1", QObject::tr("Path of the database to merge into.")); parser.addPositionalArgument("database2", QObject::tr("Path of the database to merge from.")); + parser.addOption(Command::QuietOption); QCommandLineOption samePasswordOption(QStringList() << "s" << "same-credentials", QObject::tr("Use the same credentials for both database files.")); - + parser.addOption(samePasswordOption); QCommandLineOption keyFile(QStringList() << "k" << "key-file", QObject::tr("Key file of the database."), QObject::tr("path")); @@ -58,7 +59,6 @@ int Merge::execute(const QStringList& arguments) QObject::tr("path")); parser.addOption(keyFileFrom); - parser.addOption(samePasswordOption); parser.addHelpOption(); parser.process(arguments); @@ -68,14 +68,20 @@ int Merge::execute(const QStringList& arguments) return EXIT_FAILURE; } - auto db1 = Database::unlockFromStdin(args.at(0), parser.value(keyFile), Utils::STDOUT, Utils::STDERR); + auto db1 = Database::unlockFromStdin(args.at(0), + parser.value(keyFile), + parser.isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT, + Utils::STDERR); if (!db1) { return EXIT_FAILURE; } QSharedPointer db2; if (!parser.isSet("same-credentials")) { - db2 = Database::unlockFromStdin(args.at(1), parser.value(keyFileFrom), Utils::STDOUT, Utils::STDERR); + db2 = Database::unlockFromStdin(args.at(1), + parser.value(keyFileFrom), + parser.isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT, + Utils::STDERR); } else { db2 = QSharedPointer::create(); QString errorMessage; @@ -94,8 +100,10 @@ int Merge::execute(const QStringList& arguments) err << QObject::tr("Unable to save database to file : %1").arg(errorMessage) << endl; return EXIT_FAILURE; } - out << "Successfully merged the database files." << endl; - } else { + if (!parser.isSet(Command::QuietOption)) { + out << "Successfully merged the database files." << endl; + } + } else if (!parser.isSet(Command::QuietOption)) { out << "Database was not modified by merge operation." << endl; } diff --git a/src/cli/Remove.cpp b/src/cli/Remove.cpp index 9f4877e5b..7e36d1e4a 100644 --- a/src/cli/Remove.cpp +++ b/src/cli/Remove.cpp @@ -49,6 +49,7 @@ int Remove::execute(const QStringList& arguments) QCommandLineParser parser; parser.setApplicationDescription(QCoreApplication::tr("main", "Remove an entry from the database.")); parser.addPositionalArgument("database", QCoreApplication::tr("main", "Path of the database.")); + parser.addOption(Command::QuietOption); QCommandLineOption keyFile(QStringList() << "k" << "key-file", QObject::tr("Key file of the database."), QObject::tr("path")); @@ -63,17 +64,20 @@ int Remove::execute(const QStringList& arguments) return EXIT_FAILURE; } - auto db = Database::unlockFromStdin(args.at(0), parser.value(keyFile), Utils::STDOUT, Utils::STDERR); + auto db = Database::unlockFromStdin(args.at(0), + parser.value(keyFile), + parser.isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT, + Utils::STDERR); if (!db) { return EXIT_FAILURE; } - return removeEntry(db.data(), args.at(0), args.at(1)); + return removeEntry(db.data(), args.at(0), args.at(1), parser.isSet(Command::QuietOption)); } -int Remove::removeEntry(Database* database, const QString& databasePath, const QString& entryPath) +int Remove::removeEntry(Database* database, const QString& databasePath, const QString& entryPath, bool quiet) { - TextStream out(Utils::STDOUT, QIODevice::WriteOnly); + TextStream out(quiet ? Utils::DEVNULL : Utils::STDOUT, QIODevice::WriteOnly); TextStream err(Utils::STDERR, QIODevice::WriteOnly); QPointer entry = database->rootGroup()->findEntryByPath(entryPath); diff --git a/src/cli/Remove.h b/src/cli/Remove.h index d45e04af2..fa8bc7bd6 100644 --- a/src/cli/Remove.h +++ b/src/cli/Remove.h @@ -28,7 +28,7 @@ public: Remove(); ~Remove(); int execute(const QStringList& arguments) override; - int removeEntry(Database* database, const QString& databasePath, const QString& entryPath); + int removeEntry(Database* database, const QString& databasePath, const QString& entryPath, bool quiet); }; #endif // KEEPASSXC_REMOVE_H diff --git a/src/cli/Show.cpp b/src/cli/Show.cpp index 3678d0e3d..cf8dc895d 100644 --- a/src/cli/Show.cpp +++ b/src/cli/Show.cpp @@ -22,12 +22,12 @@ #include +#include "Utils.h" #include "cli/TextStream.h" #include "core/Database.h" #include "core/Entry.h" -#include "core/Group.h" #include "core/Global.h" -#include "Utils.h" +#include "core/Group.h" Show::Show() { @@ -50,6 +50,7 @@ int Show::execute(const QStringList& arguments) QObject::tr("Key file of the database."), QObject::tr("path")); parser.addOption(keyFile); + parser.addOption(Command::QuietOption); QCommandLineOption totp(QStringList() << "t" << "totp", QObject::tr("Show the entry's current TOTP.")); parser.addOption(totp); @@ -71,7 +72,10 @@ int Show::execute(const QStringList& arguments) return EXIT_FAILURE; } - auto db = Database::unlockFromStdin(args.at(0), parser.value(keyFile), Utils::STDOUT, Utils::STDERR); + auto db = Database::unlockFromStdin(args.at(0), + parser.value(keyFile), + parser.isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT, + Utils::STDERR); if (!db) { return EXIT_FAILURE; } diff --git a/src/cli/TextStream.h b/src/cli/TextStream.h index 971f8651d..2dc116352 100644 --- a/src/cli/TextStream.h +++ b/src/cli/TextStream.h @@ -47,4 +47,4 @@ private: void detectCodec(); }; -#endif //KEEPASSXC_TEXTSTREAM_H +#endif // KEEPASSXC_TEXTSTREAM_H diff --git a/src/cli/Utils.cpp b/src/cli/Utils.cpp index a7bf83c13..7359f139c 100644 --- a/src/cli/Utils.cpp +++ b/src/cli/Utils.cpp @@ -43,6 +43,16 @@ FILE* STDERR = stderr; */ FILE* STDIN = stdin; +/** + * DEVNULL file handle for the CLI. + */ +#ifdef Q_OS_WIN +FILE* DEVNULL = fopen("nul", "w"); +#else +FILE* DEVNULL = fopen("/dev/null", "w"); +#endif + + void setStdinEcho(bool enable = true) { #ifdef Q_OS_WIN @@ -95,9 +105,9 @@ void setNextPassword(const QString& password) * * @return the password */ -QString getPassword() +QString getPassword(FILE* outputDescriptor) { - TextStream out(STDOUT, QIODevice::WriteOnly); + TextStream out(outputDescriptor, QIODevice::WriteOnly); // return preset password if one is set if (!Test::nextPasswords.isEmpty()) { diff --git a/src/cli/Utils.h b/src/cli/Utils.h index a5c995f10..8ea55da1e 100644 --- a/src/cli/Utils.h +++ b/src/cli/Utils.h @@ -26,9 +26,10 @@ namespace Utils extern FILE* STDOUT; extern FILE* STDERR; extern FILE* STDIN; +extern FILE* DEVNULL; void setStdinEcho(bool enable); -QString getPassword(); +QString getPassword(FILE* outputDescriptor = STDOUT); int clipText(const QString& text); namespace Test diff --git a/src/cli/keepassxc-cli.1 b/src/cli/keepassxc-cli.1 index 0d618c9d1..d7b0f9ada 100644 --- a/src/cli/keepassxc-cli.1 +++ b/src/cli/keepassxc-cli.1 @@ -56,6 +56,9 @@ Shows the title, username, password, URL and notes of a database entry. Can also .IP "-k, --key-file " Specifies a path to a key file for unlocking the database. In a merge operation this option is used to specify the key file path for the first database. +.IP "-q, --quiet " +Silence password prompt and other secondary outputs. + .IP "-h, --help" Displays help information. diff --git a/src/core/Database.cpp b/src/core/Database.cpp index bbea11d54..6d475acf9 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -708,10 +708,9 @@ QSharedPointer Database::unlockFromStdin(const QString& databaseFilena TextStream out(outputDescriptor); TextStream err(errorDescriptor); - out << QObject::tr("Insert password to unlock %1: ").arg(databaseFilename); - out.flush(); + out << QObject::tr("Insert password to unlock %1: ").arg(databaseFilename) << flush; - QString line = Utils::getPassword(); + QString line = Utils::getPassword(outputDescriptor); auto passwordKey = QSharedPointer::create(); passwordKey->setPassword(line); compositeKey->addKey(passwordKey); diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp index 3c5afc347..d25201e2e 100644 --- a/tests/TestCli.cpp +++ b/tests/TestCli.cpp @@ -161,6 +161,9 @@ void TestCli::testAdd() Utils::Test::setNextPassword("a"); addCmd.execute({"add", "-u", "newuser", "--url", "https://example.com/", "-g", "-l", "20", m_dbFile->fileName(), "/newuser-entry"}); m_stderrFile->reset(); + m_stdoutFile->reset(); + m_stdoutFile->readLine(); // skip password prompt + QCOMPARE(m_stdoutFile->readAll(), QByteArray("Successfully added entry newuser-entry.\n")); auto db = readTestDatabase(); auto* entry = db->rootGroup()->findEntryByPath("/newuser-entry"); @@ -169,6 +172,16 @@ void TestCli::testAdd() QCOMPARE(entry->url(), QString("https://example.com/")); QCOMPARE(entry->password().size(), 20); + // Quiet option + qint64 pos = m_stdoutFile->pos(); + Utils::Test::setNextPassword("a"); + addCmd.execute({"add", "-q", "-u", "newuser", "-g", "-l", "20", m_dbFile->fileName(), "/newentry-quiet"}); + m_stdoutFile->seek(pos); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); + db = readTestDatabase(); + entry = db->rootGroup()->findEntryByPath("/newentry-quiet"); + QVERIFY(entry); + Utils::Test::setNextPassword("a"); Utils::Test::setNextPassword("newpassword"); addCmd.execute({"add", "-u", "newuser2", "--url", "https://example.net/", "-g", "-l", "20", "-p", m_dbFile->fileName(), "/newuser-entry2"}); @@ -181,7 +194,8 @@ void TestCli::testAdd() QCOMPARE(entry->password(), QString("newpassword")); } -bool isTOTP(const QString & value) { +bool isTOTP(const QString& value) +{ QString val = value.trimmed(); if (val.length() < 5 || val.length() > 6) { return false; @@ -208,6 +222,7 @@ void TestCli::testClip() clipCmd.execute({"clip", m_dbFile->fileName(), "/Sample Entry"}); m_stderrFile->reset(); + m_stdoutFile->reset(); QString errorOutput(m_stderrFile->readAll()); if (errorOutput.contains("Unable to start program") @@ -215,6 +230,17 @@ void TestCli::testClip() QSKIP("Clip test skipped due to missing clipboard tool"); } + QCOMPARE(clipboard->text(), QString("Password")); + m_stdoutFile->readLine(); // skip prompt line + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Entry's password copied to the clipboard!\n")); + + // Quiet option + qint64 pos = m_stdoutFile->pos(); + Utils::Test::setNextPassword("a"); + clipCmd.execute({"clip", m_dbFile->fileName(), "/Sample Entry", "-q"}); + m_stdoutFile->seek(pos); + // Output should be empty when quiet option is set. + QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); QCOMPARE(clipboard->text(), QString("Password")); // TOTP @@ -284,7 +310,7 @@ void TestCli::testDiceware() const auto words = passphrase.split(" "); QCOMPARE(words.size(), 11); QRegularExpression regex("^word\\d+$"); - for (const auto& word: words) { + for (const auto& word : words) { QVERIFY2(regex.match(word).hasMatch(), qPrintable("Word " + word + " was not on the word list")); } } @@ -297,6 +323,9 @@ void TestCli::testEdit() Utils::Test::setNextPassword("a"); editCmd.execute({"edit", "-u", "newuser", "--url", "https://otherurl.example.com/", "-t", "newtitle", m_dbFile->fileName(), "/Sample Entry"}); + m_stdoutFile->reset(); + m_stdoutFile->readLine(); // skip prompt line + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Successfully edited entry newtitle.\n")); auto db = readTestDatabase(); auto* entry = db->rootGroup()->findEntryByPath("/newtitle"); @@ -305,6 +334,13 @@ void TestCli::testEdit() QCOMPARE(entry->url(), QString("https://otherurl.example.com/")); QCOMPARE(entry->password(), QString("Password")); + // Quiet option + qint64 pos = m_stdoutFile->pos(); + Utils::Test::setNextPassword("a"); + editCmd.execute({"edit", m_dbFile->fileName(), "-q", "-t", "newtitle", "/Sample Entry"}); + m_stdoutFile->seek(pos); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); + Utils::Test::setNextPassword("a"); editCmd.execute({"edit", "-g", m_dbFile->fileName(), "/newtitle"}); db = readTestDatabase(); @@ -430,7 +466,7 @@ void TestCli::testEstimate() QVERIFY(result.startsWith("Length " + length)); QVERIFY(result.contains("Entropy " + entropy)); QVERIFY(result.contains("Log10 " + log10)); - for (const auto& string: asConst(searchStrings)) { + for (const auto& string : asConst(searchStrings)) { QVERIFY2(result.contains(string), qPrintable("String " + string + " missing")); } } @@ -455,6 +491,18 @@ void TestCli::testExtract() auto* entry = db->rootGroup()->findEntryByPath("/Sample Entry"); QVERIFY(entry); QCOMPARE(entry->password(), QString("Password")); + + m_stdoutFile->reset(); + + // Quiet option + QScopedPointer dbQuiet(new Database()); + qint64 pos = m_stdoutFile->pos(); + Utils::Test::setNextPassword("a"); + extractCmd.execute({"extract", "-q", m_dbFile->fileName()}); + m_stdoutFile->seek(pos); + reader.readDatabase(m_stdoutFile.data(), dbQuiet.data()); + QVERIFY(!reader.hasError()); + QVERIFY(db.data()); } void TestCli::testGenerate_data() @@ -533,8 +581,22 @@ void TestCli::testList() "eMail/\n" "Homebanking/\n")); + // Quiet option qint64 pos = m_stdoutFile->pos(); Utils::Test::setNextPassword("a"); + listCmd.execute({"ls", "-q", m_dbFile->fileName()}); + m_stdoutFile->seek(pos); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("Sample Entry\n" + "General/\n" + "Windows/\n" + "Network/\n" + "Internet/\n" + "eMail/\n" + "Homebanking/\n")); + + + pos = m_stdoutFile->pos(); + Utils::Test::setNextPassword("a"); listCmd.execute({"ls", "-R", m_dbFile->fileName()}); m_stdoutFile->seek(pos); m_stdoutFile->readLine(); // skip password prompt @@ -581,8 +643,15 @@ void TestCli::testLocate() m_stdoutFile->readLine(); // skip password prompt QCOMPARE(m_stdoutFile->readAll(), QByteArray("/Sample Entry\n")); + // Quiet option qint64 pos = m_stdoutFile->pos(); Utils::Test::setNextPassword("a"); + locateCmd.execute({"locate", m_dbFile->fileName(), "-q", "Sample"}); + m_stdoutFile->seek(pos); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("/Sample Entry\n")); + + pos = m_stdoutFile->pos(); + Utils::Test::setNextPassword("a"); locateCmd.execute({"locate", m_dbFile->fileName(), "Does Not Exist"}); m_stdoutFile->seek(pos); m_stdoutFile->readLine(); // skip password prompt @@ -709,6 +778,13 @@ void TestCli::testMerge() m_stdoutFile->seek(pos); m_stdoutFile->readLine(); QCOMPARE(m_stdoutFile->readAll(), QByteArray("Database was not modified by merge operation.\n")); + + // Quiet option + pos = m_stdoutFile->pos(); + Utils::Test::setNextPassword("a"); + mergeCmd.execute({"merge", "-q", "-s", sourceFile.fileName(), sourceFile.fileName()}); + m_stdoutFile->seek(pos); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); } void TestCli::testRemove() @@ -779,6 +855,52 @@ void TestCli::testRemove() QCOMPARE(m_stderrFile->readAll(), QByteArray("Entry /Sample Entry not found.\n")); } +void TestCli::testRemoveQuiet() +{ + Remove removeCmd; + QVERIFY(!removeCmd.name.isEmpty()); + QVERIFY(removeCmd.getDescriptionLine().contains(removeCmd.name)); + + Kdbx3Reader reader; + Kdbx3Writer writer; + + qint64 pos = m_stdoutFile->pos(); + + // delete entry and verify + Utils::Test::setNextPassword("a"); + removeCmd.execute({"rm", "-q", m_dbFile->fileName(), "/Sample Entry"}); + m_stdoutFile->seek(pos); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); + + auto key = QSharedPointer::create(); + key->addKey(QSharedPointer::create("a")); + QFile readBack(m_dbFile->fileName()); + readBack.open(QIODevice::ReadOnly); + auto readBackDb = QSharedPointer::create(); + reader.readDatabase(&readBack, key, readBackDb.data()); + readBack.close(); + QVERIFY(readBackDb); + QVERIFY(!readBackDb->rootGroup()->findEntryByPath("/Sample Entry")); + QVERIFY(readBackDb->rootGroup()->findEntryByPath("/Recycle Bin/Sample Entry")); + + pos = m_stdoutFile->pos(); + + // remove the entry completely + Utils::Test::setNextPassword("a"); + removeCmd.execute({"rm", "-q", m_dbFile->fileName(), "/Recycle Bin/Sample Entry"}); + m_stdoutFile->seek(pos); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); + + readBack.setFileName(m_dbFile->fileName()); + readBack.open(QIODevice::ReadOnly); + readBackDb = QSharedPointer::create(); + reader.readDatabase(&readBack, key, readBackDb.data()); + readBack.close(); + QVERIFY(readBackDb); + QVERIFY(!readBackDb->rootGroup()->findEntryByPath("/Sample Entry")); + QVERIFY(!readBackDb->rootGroup()->findEntryByPath("/Recycle Bin/Sample Entry")); +} + void TestCli::testShow() { Show showCmd; @@ -797,6 +919,16 @@ void TestCli::testShow() qint64 pos = m_stdoutFile->pos(); Utils::Test::setNextPassword("a"); + showCmd.execute({"show", m_dbFile->fileName(), "-q", "/Sample Entry"}); + m_stdoutFile->seek(pos); + 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")); + + pos = m_stdoutFile->pos(); + Utils::Test::setNextPassword("a"); showCmd.execute({"show", "-a", "Title", m_dbFile->fileName(), "/Sample Entry"}); m_stdoutFile->seek(pos); m_stdoutFile->readLine(); // skip password prompt diff --git a/tests/TestCli.h b/tests/TestCli.h index 2c411f2ca..061055dcd 100644 --- a/tests/TestCli.h +++ b/tests/TestCli.h @@ -54,6 +54,7 @@ private slots: void testLocate(); void testMerge(); void testRemove(); + void testRemoveQuiet(); void testShow(); private: