From 68674da4e3f75bbadd92ebbbfba368b859f92ac9 Mon Sep 17 00:00:00 2001 From: louib Date: Thu, 12 Sep 2019 11:08:28 -0400 Subject: [PATCH] CLI: Add group commands --- CHANGELOG.md | 1 + src/cli/AddGroup.cpp | 80 ++++++++++++++ src/cli/AddGroup.h | 32 ++++++ src/cli/CMakeLists.txt | 3 + src/cli/Command.cpp | 6 ++ src/cli/Move.cpp | 81 ++++++++++++++ src/cli/Move.h | 32 ++++++ src/cli/RemoveGroup.cpp | 85 +++++++++++++++ src/cli/RemoveGroup.h | 32 ++++++ src/cli/keepassxc-cli.1 | 9 ++ tests/TestCli.cpp | 231 +++++++++++++++++++++++++++++++++++++--- tests/TestCli.h | 3 + 12 files changed, 582 insertions(+), 13 deletions(-) create mode 100644 src/cli/AddGroup.cpp create mode 100644 src/cli/AddGroup.h create mode 100644 src/cli/Move.cpp create mode 100644 src/cli/Move.h create mode 100644 src/cli/RemoveGroup.cpp create mode 100644 src/cli/RemoveGroup.h diff --git a/CHANGELOG.md b/CHANGELOG.md index f9e1b006c4..ff2f16dd04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - CLI: Add password generation options to `Add` and `Edit` commands [#3275](https://github.com/keepassxreboot/keepassxc/issues/3275) - CLI: Add CSV export to the 'export' command [#3277] - Add 'Monospaced font' option to the Notes field [#3321](https://github.com/keepassxreboot/keepassxc/issues/3321) +- CLI: Add group commands (mv, mkdir and rmdir) [#3313]. ### Changed diff --git a/src/cli/AddGroup.cpp b/src/cli/AddGroup.cpp new file mode 100644 index 0000000000..02653fd3cb --- /dev/null +++ b/src/cli/AddGroup.cpp @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2019 KeePassXC Team + * + * 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 . + */ + +#include +#include + +#include "AddGroup.h" + +#include "cli/TextStream.h" +#include "cli/Utils.h" +#include "core/Database.h" +#include "core/Entry.h" +#include "core/Group.h" + +AddGroup::AddGroup() +{ + name = QString("mkdir"); + description = QObject::tr("Adds a new group to a database."); + positionalArguments.append({QString("group"), QObject::tr("Path of the group to add."), QString("")}); +} + +AddGroup::~AddGroup() +{ +} + +int AddGroup::executeWithDatabase(QSharedPointer database, QSharedPointer parser) +{ + TextStream outputTextStream(Utils::STDOUT, QIODevice::WriteOnly); + TextStream errorTextStream(Utils::STDERR, QIODevice::WriteOnly); + + const QStringList args = parser->positionalArguments(); + const QString& databasePath = args.at(0); + const QString& groupPath = args.at(1); + + QStringList pathParts = groupPath.split("/"); + QString groupName = pathParts.takeLast(); + QString parentGroupPath = pathParts.join("/"); + + Group* group = database->rootGroup()->findGroupByPath(groupPath); + if (group) { + errorTextStream << QObject::tr("Group %1 already exists!").arg(groupPath) << endl; + return EXIT_FAILURE; + } + + Group* parentGroup = database->rootGroup()->findGroupByPath(parentGroupPath); + if (!parentGroup) { + errorTextStream << QObject::tr("Group %1 not found.").arg(parentGroupPath) << endl; + return EXIT_FAILURE; + } + + Group* newGroup = new Group(); + newGroup->setUuid(QUuid::createUuid()); + newGroup->setName(groupName); + newGroup->setParent(parentGroup); + + QString errorMessage; + if (!database->save(databasePath, &errorMessage, true, false)) { + errorTextStream << QObject::tr("Writing the database failed %1.").arg(errorMessage) << endl; + return EXIT_FAILURE; + } + + if (!parser->isSet(Command::QuietOption)) { + outputTextStream << QObject::tr("Successfully added group %1.").arg(groupName) << endl; + } + return EXIT_SUCCESS; +} diff --git a/src/cli/AddGroup.h b/src/cli/AddGroup.h new file mode 100644 index 0000000000..9976d58942 --- /dev/null +++ b/src/cli/AddGroup.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2019 KeePassXC Team + * + * 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 . + */ + +#ifndef KEEPASSXC_ADDGROUP_H +#define KEEPASSXC_ADDGROUP_H + +#include "DatabaseCommand.h" + +class AddGroup : public DatabaseCommand +{ +public: + AddGroup(); + ~AddGroup(); + + int executeWithDatabase(QSharedPointer db, QSharedPointer parser); +}; + +#endif // KEEPASSXC_ADDGROUP_H diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt index 4a8b28c3b7..8d3c0d69f2 100644 --- a/src/cli/CMakeLists.txt +++ b/src/cli/CMakeLists.txt @@ -15,6 +15,7 @@ set(cli_SOURCES Add.cpp + AddGroup.cpp Analyze.cpp Clip.cpp Create.cpp @@ -28,7 +29,9 @@ set(cli_SOURCES List.cpp Locate.cpp Merge.cpp + Move.cpp Remove.cpp + RemoveGroup.cpp Show.cpp) add_library(cli STATIC ${cli_SOURCES}) diff --git a/src/cli/Command.cpp b/src/cli/Command.cpp index fdea26e65f..9d36e602ed 100644 --- a/src/cli/Command.cpp +++ b/src/cli/Command.cpp @@ -23,6 +23,7 @@ #include "Command.h" #include "Add.h" +#include "AddGroup.h" #include "Analyze.h" #include "Clip.h" #include "Create.h" @@ -34,7 +35,9 @@ #include "List.h" #include "Locate.h" #include "Merge.h" +#include "Move.h" #include "Remove.h" +#include "RemoveGroup.h" #include "Show.h" #include "TextStream.h" #include "Utils.h" @@ -119,7 +122,10 @@ void populateCommands() commands.insert(QString("locate"), new Locate()); commands.insert(QString("ls"), new List()); commands.insert(QString("merge"), new Merge()); + commands.insert(QString("mkdir"), new AddGroup()); + commands.insert(QString("mv"), new Move()); commands.insert(QString("rm"), new Remove()); + commands.insert(QString("rmdir"), new RemoveGroup()); commands.insert(QString("show"), new Show()); } } diff --git a/src/cli/Move.cpp b/src/cli/Move.cpp new file mode 100644 index 0000000000..29e9a98fde --- /dev/null +++ b/src/cli/Move.cpp @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2019 KeePassXC Team + * + * 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 . + */ + +#include +#include + +#include "Move.h" + +#include "cli/TextStream.h" +#include "cli/Utils.h" +#include "core/Database.h" +#include "core/Entry.h" +#include "core/Group.h" + +Move::Move() +{ + name = QString("mv"); + description = QObject::tr("Moves an entry to a new group."); + positionalArguments.append({QString("entry"), QObject::tr("Path of the entry to move."), QString("")}); + positionalArguments.append({QString("group"), QObject::tr("Path of the destination group."), QString("")}); +} + +Move::~Move() +{ +} + +int Move::executeWithDatabase(QSharedPointer database, QSharedPointer parser) +{ + TextStream outputTextStream(Utils::STDOUT, QIODevice::WriteOnly); + TextStream errorTextStream(Utils::STDERR, QIODevice::WriteOnly); + + const QStringList args = parser->positionalArguments(); + const QString& databasePath = args.at(0); + const QString& entryPath = args.at(1); + const QString& destinationPath = args.at(2); + + Entry* entry = database->rootGroup()->findEntryByPath(entryPath); + if (!entry) { + errorTextStream << QObject::tr("Could not find entry with path %1.").arg(entryPath) << endl; + return EXIT_FAILURE; + } + + Group* destinationGroup = database->rootGroup()->findGroupByPath(destinationPath); + if (!destinationGroup) { + errorTextStream << QObject::tr("Could not find group with path %1.").arg(destinationPath) << endl; + return EXIT_FAILURE; + } + + if (destinationGroup == entry->parent()) { + errorTextStream << QObject::tr("Entry is already in group %1.").arg(destinationPath) << endl; + return EXIT_FAILURE; + } + + entry->beginUpdate(); + entry->setGroup(destinationGroup); + entry->endUpdate(); + + QString errorMessage; + if (!database->save(databasePath, &errorMessage, true, false)) { + errorTextStream << QObject::tr("Writing the database failed %1.").arg(errorMessage) << endl; + return EXIT_FAILURE; + } + + outputTextStream << QObject::tr("Successfully moved entry %1 to group %2.").arg(entry->title(), destinationPath) + << endl; + return EXIT_SUCCESS; +} diff --git a/src/cli/Move.h b/src/cli/Move.h new file mode 100644 index 0000000000..c506085a54 --- /dev/null +++ b/src/cli/Move.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2019 KeePassXC Team + * + * 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 . + */ + +#ifndef KEEPASSXC_MOVE_H +#define KEEPASSXC_MOVE_H + +#include "DatabaseCommand.h" + +class Move : public DatabaseCommand +{ +public: + Move(); + ~Move(); + + int executeWithDatabase(QSharedPointer db, QSharedPointer parser); +}; + +#endif // KEEPASSXC_MOVE_H diff --git a/src/cli/RemoveGroup.cpp b/src/cli/RemoveGroup.cpp new file mode 100644 index 0000000000..4f48fb07ce --- /dev/null +++ b/src/cli/RemoveGroup.cpp @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2019 KeePassXC Team + * + * 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 . + */ + +#include +#include + +#include "RemoveGroup.h" + +#include "cli/TextStream.h" +#include "cli/Utils.h" +#include "core/Database.h" +#include "core/Entry.h" +#include "core/Group.h" +#include "core/Metadata.h" +#include "core/Tools.h" + +RemoveGroup::RemoveGroup() +{ + name = QString("rmdir"); + description = QString("Removes a group from a database."); + positionalArguments.append({QString("group"), QObject::tr("Path of the group to remove."), QString("")}); +} + +RemoveGroup::~RemoveGroup() +{ +} + +int RemoveGroup::executeWithDatabase(QSharedPointer database, QSharedPointer parser) +{ + bool quiet = parser->isSet(Command::QuietOption); + QString databasePath = parser->positionalArguments().at(0); + QString groupPath = parser->positionalArguments().at(1); + + TextStream outputTextStream(quiet ? Utils::DEVNULL : Utils::STDOUT, QIODevice::WriteOnly); + TextStream errorTextStream(Utils::STDERR, QIODevice::WriteOnly); + + // Recursive option means were looking for a group to remove. + QPointer group = database->rootGroup()->findGroupByPath(groupPath); + if (!group) { + errorTextStream << QObject::tr("Group %1 not found.").arg(groupPath) << endl; + return EXIT_FAILURE; + } + + if (group == database->rootGroup()) { + errorTextStream << QObject::tr("Cannot remove root group from database.") << endl; + return EXIT_FAILURE; + } + + bool recycled = true; + auto* recycleBin = database->metadata()->recycleBin(); + if (!database->metadata()->recycleBinEnabled() || (recycleBin && recycleBin->findGroupByUuid(group->uuid()))) { + delete group; + recycled = false; + } else { + database->recycleGroup(group); + }; + + QString errorMessage; + if (!database->save(databasePath, &errorMessage, true, false)) { + errorTextStream << QObject::tr("Unable to save database to file: %1").arg(errorMessage) << endl; + return EXIT_FAILURE; + } + + if (recycled) { + outputTextStream << QObject::tr("Successfully recycled group %1.").arg(groupPath) << endl; + } else { + outputTextStream << QObject::tr("Successfully deleted group %1.").arg(groupPath) << endl; + } + + return EXIT_SUCCESS; +} diff --git a/src/cli/RemoveGroup.h b/src/cli/RemoveGroup.h new file mode 100644 index 0000000000..2b51946654 --- /dev/null +++ b/src/cli/RemoveGroup.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017 KeePassXC Team + * + * 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 . + */ + +#ifndef KEEPASSXC_REMOVEGROUP_H +#define KEEPASSXC_REMOVEGROUP_H + +#include "DatabaseCommand.h" + +class RemoveGroup : public DatabaseCommand +{ +public: + RemoveGroup(); + ~RemoveGroup(); + + int executeWithDatabase(QSharedPointer db, QSharedPointer parser); +}; + +#endif // KEEPASSXC_REMOVEGROUP_H diff --git a/src/cli/keepassxc-cli.1 b/src/cli/keepassxc-cli.1 index 24b9b0f764..ea6c26fed7 100644 --- a/src/cli/keepassxc-cli.1 +++ b/src/cli/keepassxc-cli.1 @@ -51,9 +51,18 @@ Lists the contents of a group in a database. If no group is specified, it will d .IP "merge [options] " Merges two databases together. The first database file is going to be replaced by the result of the merge, for that reason it is advisable to keep a backup of the two database files before attempting a merge. In the case that both databases make use of the same credentials, the \fI--same-credentials\fP or \fI-s\fP option can be used. +.IP "mkdir [options] " +Adds a new group to a database. + +.IP "mv [options] " +Moves an entry to a new group. + .IP "rm [options] " Removes an entry from a database. If the database has a recycle bin, the entry will be moved there. If the entry is already in the recycle bin, it will be removed permanently. +.IP "rmdir [options] " +Removes a group from a database. If the database has a recycle bin, the group will be moved there. If the group is already in the recycle bin, it will be removed permanently. + .IP "show [options] " Shows the title, username, password, URL and notes of a database entry. Can also show the current TOTP. Regarding the occurrence of multiple entries with the same name in different groups, everything stated in the \fIclip\fP command section also applies here. diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp index d1ae3992d9..22a98948f9 100644 --- a/tests/TestCli.cpp +++ b/tests/TestCli.cpp @@ -32,6 +32,7 @@ #include "format/KeePass2.h" #include "cli/Add.h" +#include "cli/AddGroup.h" #include "cli/Analyze.h" #include "cli/Clip.h" #include "cli/Command.h" @@ -44,7 +45,9 @@ #include "cli/List.h" #include "cli/Locate.h" #include "cli/Merge.h" +#include "cli/Move.h" #include "cli/Remove.h" +#include "cli/RemoveGroup.h" #include "cli/Show.h" #include "cli/Utils.h" @@ -162,7 +165,7 @@ QSharedPointer TestCli::readTestDatabase() const void TestCli::testCommand() { - QCOMPARE(Command::getCommands().size(), 14); + QCOMPARE(Command::getCommands().size(), 17); QVERIFY(Command::getCommand("add")); QVERIFY(Command::getCommand("analyze")); QVERIFY(Command::getCommand("clip")); @@ -298,20 +301,73 @@ void TestCli::testAdd() QVERIFY(!defaultPasswordClassesRegex.match(entry->password()).hasMatch()); } -void TestCli::testAnalyze() +void TestCli::testAddGroup() { - Analyze analyzeCmd; - QVERIFY(!analyzeCmd.name.isEmpty()); - QVERIFY(analyzeCmd.getDescriptionLine().contains(analyzeCmd.name)); - - const QString hibpPath = QString(KEEPASSX_TEST_DATA_DIR).append("/hibp.txt"); + AddGroup addGroupCmd; + QVERIFY(!addGroupCmd.name.isEmpty()); + QVERIFY(addGroupCmd.getDescriptionLine().contains(addGroupCmd.name)); Utils::Test::setNextPassword("a"); - analyzeCmd.execute({"analyze", "--hibp", hibpPath, m_dbFile->fileName()}); + addGroupCmd.execute({"mkdir", m_dbFile->fileName(), "/new_group"}); + m_stderrFile->reset(); m_stdoutFile->reset(); m_stdoutFile->readLine(); // skip password prompt - auto output = m_stdoutFile->readAll(); - QVERIFY(output.contains("Sample Entry") && output.contains("123")); + QCOMPARE(m_stderrFile->readAll(), QByteArray("")); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("Successfully added group new_group.\n")); + + auto db = readTestDatabase(); + auto* group = db->rootGroup()->findGroupByPath("new_group"); + QVERIFY(group); + QCOMPARE(group->name(), QString("new_group")); + + // Trying to add the same group should fail. + qint64 pos = m_stdoutFile->pos(); + qint64 posErr = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + addGroupCmd.execute({"mkdir", m_dbFile->fileName(), "/new_group"}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip password prompt + m_stderrFile->seek(posErr); + QCOMPARE(m_stderrFile->readAll(), QByteArray("Group /new_group already exists!\n")); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); + + // Should be able to add groups down the tree. + pos = m_stdoutFile->pos(); + posErr = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + addGroupCmd.execute({"mkdir", m_dbFile->fileName(), "/new_group/newer_group"}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip password prompt + m_stderrFile->seek(posErr); + QCOMPARE(m_stderrFile->readAll(), QByteArray("")); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("Successfully added group newer_group.\n")); + + db = readTestDatabase(); + group = db->rootGroup()->findGroupByPath("new_group/newer_group"); + QVERIFY(group); + QCOMPARE(group->name(), QString("newer_group")); + + // Should fail if the path is invalid. + pos = m_stdoutFile->pos(); + posErr = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + addGroupCmd.execute({"mkdir", m_dbFile->fileName(), "/invalid_group/newer_group"}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip password prompt + m_stderrFile->seek(posErr); + QCOMPARE(m_stderrFile->readAll(), QByteArray("Group /invalid_group not found.\n")); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); + + // Should fail to add the root group. + pos = m_stdoutFile->pos(); + posErr = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + addGroupCmd.execute({"mkdir", m_dbFile->fileName(), "/"}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip password prompt + m_stderrFile->seek(posErr); + QCOMPARE(m_stderrFile->readAll(), QByteArray("Group / already exists!\n")); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); } bool isTOTP(const QString& value) @@ -328,6 +384,22 @@ bool isTOTP(const QString& value) return true; } +void TestCli::testAnalyze() +{ + Analyze analyzeCmd; + QVERIFY(!analyzeCmd.name.isEmpty()); + QVERIFY(analyzeCmd.getDescriptionLine().contains(analyzeCmd.name)); + + const QString hibpPath = QString(KEEPASSX_TEST_DATA_DIR).append("/hibp.txt"); + + Utils::Test::setNextPassword("a"); + analyzeCmd.execute({"analyze", "--hibp", hibpPath, m_dbFile->fileName()}); + m_stdoutFile->reset(); + m_stdoutFile->readLine(); // skip password prompt + auto output = m_stdoutFile->readAll(); + QVERIFY(output.contains("Sample Entry") && output.contains("123")); +} + void TestCli::testClip() { QClipboard* clipboard = QGuiApplication::clipboard(); @@ -1246,6 +1318,63 @@ void TestCli::testMerge() QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); } +void TestCli::testMove() +{ + Move moveCmd; + QVERIFY(!moveCmd.name.isEmpty()); + QVERIFY(moveCmd.getDescriptionLine().contains(moveCmd.name)); + + qint64 pos = m_stdoutFile->pos(); + qint64 posErr = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + moveCmd.execute({"mv", m_dbFile->fileName(), "invalid_entry_path", "invalid_group_path"}); + m_stdoutFile->seek(pos); + m_stderrFile->seek(posErr); + m_stdoutFile->readLine(); // skip prompt line + QCOMPARE(m_stdoutFile->readLine(), QByteArray("")); + QCOMPARE(m_stderrFile->readLine(), QByteArray("Could not find entry with path invalid_entry_path.\n")); + + pos = m_stdoutFile->pos(); + posErr = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + moveCmd.execute({"mv", m_dbFile->fileName(), "Sample Entry", "invalid_group_path"}); + m_stdoutFile->seek(pos); + m_stderrFile->seek(posErr); + m_stdoutFile->readLine(); // skip prompt line + QCOMPARE(m_stdoutFile->readLine(), QByteArray("")); + QCOMPARE(m_stderrFile->readLine(), QByteArray("Could not find group with path invalid_group_path.\n")); + + pos = m_stdoutFile->pos(); + posErr = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + moveCmd.execute({"mv", m_dbFile->fileName(), "Sample Entry", "General/"}); + m_stdoutFile->seek(pos); + m_stderrFile->seek(posErr); + m_stdoutFile->readLine(); // skip prompt line + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Successfully moved entry Sample Entry to group General/.\n")); + QCOMPARE(m_stderrFile->readLine(), QByteArray("")); + + auto db = readTestDatabase(); + auto* entry = db->rootGroup()->findEntryByPath("General/Sample Entry"); + QVERIFY(entry); + + // Test that not modified if the same group is destination. + pos = m_stdoutFile->pos(); + posErr = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + moveCmd.execute({"mv", m_dbFile->fileName(), "General/Sample Entry", "General/"}); + m_stdoutFile->seek(pos); + m_stderrFile->seek(posErr); + m_stdoutFile->readLine(); // skip prompt line + QCOMPARE(m_stdoutFile->readLine(), QByteArray("")); + QCOMPARE(m_stderrFile->readLine(), QByteArray("Entry is already in group General/.\n")); + + // sanity check + db = readTestDatabase(); + entry = db->rootGroup()->findEntryByPath("General/Sample Entry"); + QVERIFY(entry); +} + void TestCli::testRemove() { Remove removeCmd; @@ -1265,6 +1394,7 @@ void TestCli::testRemove() fileCopy.close(); qint64 pos = m_stdoutFile->pos(); + qint64 posErr = m_stderrFile->pos(); // delete entry and verify Utils::Test::setNextPassword("a"); @@ -1272,6 +1402,7 @@ void TestCli::testRemove() m_stdoutFile->seek(pos); m_stdoutFile->readLine(); // skip password prompt QCOMPARE(m_stdoutFile->readAll(), QByteArray("Successfully recycled entry Sample Entry.\n")); + QCOMPARE(m_stderrFile->readAll(), QByteArray("")); auto key = QSharedPointer::create(); key->addKey(QSharedPointer::create("a")); @@ -1284,6 +1415,7 @@ void TestCli::testRemove() QVERIFY(!readBackDb->rootGroup()->findEntryByPath("/Sample Entry")); QVERIFY(readBackDb->rootGroup()->findEntryByPath(QString("/%1/Sample Entry").arg(Group::tr("Recycle Bin")))); + pos = m_stdoutFile->pos(); pos = m_stdoutFile->pos(); // try again, this time without recycle bin @@ -1302,16 +1434,89 @@ void TestCli::testRemove() QVERIFY(!readBackDb->rootGroup()->findEntryByPath("/Sample Entry")); QVERIFY(!readBackDb->rootGroup()->findEntryByPath(QString("/%1/Sample Entry").arg(Group::tr("Recycle Bin")))); - pos = m_stdoutFile->pos(); - // finally, try deleting a non-existent entry + pos = m_stdoutFile->pos(); + posErr = m_stderrFile->pos(); Utils::Test::setNextPassword("a"); removeCmd.execute({"rm", fileCopy.fileName(), "/Sample Entry"}); m_stdoutFile->seek(pos); m_stdoutFile->readLine(); // skip password prompt - m_stderrFile->reset(); + m_stderrFile->seek(posErr); QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); QCOMPARE(m_stderrFile->readAll(), QByteArray("Entry /Sample Entry not found.\n")); + + // try deleting a directory, should fail + pos = m_stdoutFile->pos(); + posErr = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + removeCmd.execute({"rm", fileCopy.fileName(), "/General"}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip password prompt + m_stderrFile->seek(posErr); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); + QCOMPARE(m_stderrFile->readAll(), QByteArray("Entry /General not found.\n")); +} + +void TestCli::testRemoveGroup() +{ + RemoveGroup removeGroupCmd; + QVERIFY(!removeGroupCmd.name.isEmpty()); + QVERIFY(removeGroupCmd.getDescriptionLine().contains(removeGroupCmd.name)); + + Kdbx3Reader reader; + Kdbx3Writer writer; + + // try deleting a directory, should recycle it first. + qint64 pos = m_stdoutFile->pos(); + qint64 posErr = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + removeGroupCmd.execute({"rmdir", m_dbFile->fileName(), "/General"}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip password prompt + m_stderrFile->seek(posErr); + QCOMPARE(m_stderrFile->readAll(), QByteArray("")); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("Successfully recycled group /General.\n")); + + auto db = readTestDatabase(); + auto* group = db->rootGroup()->findGroupByPath("General"); + QVERIFY(!group); + + // try deleting a directory again, should delete it permanently. + pos = m_stdoutFile->pos(); + posErr = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + removeGroupCmd.execute({"rmdir", m_dbFile->fileName(), "Recycle Bin/General"}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip password prompt + m_stderrFile->seek(posErr); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("Successfully deleted group Recycle Bin/General.\n")); + QCOMPARE(m_stderrFile->readAll(), QByteArray("")); + + db = readTestDatabase(); + group = db->rootGroup()->findGroupByPath("Recycle Bin/General"); + QVERIFY(!group); + + // try deleting an invalid group, should fail. + pos = m_stdoutFile->pos(); + posErr = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + removeGroupCmd.execute({"rmdir", m_dbFile->fileName(), "invalid"}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip password prompt + m_stderrFile->seek(posErr); + QCOMPARE(m_stderrFile->readAll(), QByteArray("Group invalid not found.\n")); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); + + // Should fail to remove the root group. + pos = m_stdoutFile->pos(); + posErr = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + removeGroupCmd.execute({"rmdir", m_dbFile->fileName(), "/"}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip password prompt + m_stderrFile->seek(posErr); + QCOMPARE(m_stderrFile->readAll(), QByteArray("Cannot remove root group from database.\n")); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); } void TestCli::testRemoveQuiet() diff --git a/tests/TestCli.h b/tests/TestCli.h index 09c55e0edb..434cb131d6 100644 --- a/tests/TestCli.h +++ b/tests/TestCli.h @@ -45,6 +45,7 @@ private slots: void testCommand(); void testAdd(); + void testAddGroup(); void testAnalyze(); void testClip(); void testCreate(); @@ -60,7 +61,9 @@ private slots: void testList(); void testLocate(); void testMerge(); + void testMove(); void testRemove(); + void testRemoveGroup(); void testRemoveQuiet(); void testShow(); void testInvalidDbFiles();