diff options
author | Aetf <aetf@unlimitedcodeworks.xyz> | 2020-05-29 04:37:42 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-05-29 04:37:42 +0300 |
commit | 229a756d84cf4fd45ea3fdbcf93246e11f808d8d (patch) | |
tree | 206fb77d3d33940fd4e8902007decc17b2621880 /tests | |
parent | af6493b07b5ca9fd01da8324f589eaf3fc0dd000 (diff) | |
parent | a1f599c7c4e1d3d83c153d21d85eff6ca3a4b233 (diff) |
Merge pull request #4232 from Aetf/feature/fdo-secrets-unittests
FdoSecrets: Add unit tests for secret service integration
Diffstat (limited to 'tests')
-rw-r--r-- | tests/CMakeLists.txt | 7 | ||||
-rw-r--r-- | tests/TestEntrySearcher.cpp | 51 | ||||
-rw-r--r-- | tests/TestEntrySearcher.h | 1 | ||||
-rw-r--r-- | tests/data/dbus/interfaces/org.freedesktop.Secret.Collection.xml | 33 | ||||
-rw-r--r-- | tests/data/dbus/interfaces/org.freedesktop.Secret.Item.xml | 21 | ||||
-rw-r--r-- | tests/data/dbus/interfaces/org.freedesktop.Secret.Prompt.xml | 11 | ||||
-rw-r--r-- | tests/data/dbus/interfaces/org.freedesktop.Secret.Service.xml | 55 | ||||
-rw-r--r-- | tests/data/dbus/interfaces/org.freedesktop.Secret.Session.xml | 4 | ||||
-rw-r--r-- | tests/data/dbus/session.conf | 39 | ||||
-rw-r--r-- | tests/gui/CMakeLists.txt | 9 | ||||
-rw-r--r-- | tests/gui/TestGuiFdoSecrets.cpp | 1175 | ||||
-rw-r--r-- | tests/gui/TestGuiFdoSecrets.h | 121 |
12 files changed, 1524 insertions, 3 deletions
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e1bfaac13..28d8516cf 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -57,8 +57,9 @@ macro(parse_arguments prefix arg_names option_names) endmacro(parse_arguments) macro(add_unit_test) - parse_arguments(TEST "NAME;SOURCES;LIBS" "" ${ARGN}) + parse_arguments(TEST "NAME;SOURCES;LIBS;LAUNCHER" "" ${ARGN}) set(_test_NAME ${TEST_NAME}) + set(_test_LAUNCHER ${TEST_LAUNCHER}) set(_srcList ${TEST_SOURCES}) add_executable(${_test_NAME} ${_srcList}) target_link_libraries(${_test_NAME} ${TEST_LIBS}) @@ -69,9 +70,9 @@ macro(add_unit_test) set(TEST_OUTPUT ${TEST_OUTPUT} CACHE STRING "The output to generate when running the QTest unit tests") if(KDE4_TEST_OUTPUT STREQUAL "xml") - add_test(${_test_NAME} ${_test_NAME} -xml -o ${_test_NAME}.tml) + add_test(${_test_NAME} ${_test_LAUNCHER} ${_test_NAME} -xml -o ${_test_NAME}.tml) else(KDE4_TEST_OUTPUT STREQUAL "xml") - add_test(${_test_NAME} ${_test_NAME}) + add_test(${_test_NAME} ${_test_LAUNCHER} ${_test_NAME}) endif(KDE4_TEST_OUTPUT STREQUAL "xml") if(NOT MSVC_IDE) #not needed for the ide diff --git a/tests/TestEntrySearcher.cpp b/tests/TestEntrySearcher.cpp index 3a0ac6836..1b8b4f5b8 100644 --- a/tests/TestEntrySearcher.cpp +++ b/tests/TestEntrySearcher.cpp @@ -23,6 +23,7 @@ QTEST_GUILESS_MAIN(TestEntrySearcher) void TestEntrySearcher::init() { m_rootGroup = new Group(); + m_entrySearcher = EntrySearcher(); } void TestEntrySearcher::cleanup() @@ -259,6 +260,7 @@ void TestEntrySearcher::testCustomAttributesAreSearched() QCOMPARE(m_searchResult.count(), 2); // protected attributes are ignored + m_entrySearcher = EntrySearcher(false, true); m_searchResult = m_entrySearcher.search("_testAttribute:test _testProtected:testP2", m_rootGroup); QCOMPARE(m_searchResult.count(), 2); } @@ -319,3 +321,52 @@ void TestEntrySearcher::testGroup() m_searchResult = m_entrySearcher.search("g:/group1 search", m_rootGroup); QCOMPARE(m_searchResult.count(), 1); } + +void TestEntrySearcher::testSkipProtected() +{ + QScopedPointer<Entry> e1(new Entry()); + e1->setGroup(m_rootGroup); + + e1->attributes()->set("testAttribute", "testE1"); + e1->attributes()->set("testProtected", "apple", true); + + QScopedPointer<Entry> e2(new Entry()); + e2->setGroup(m_rootGroup); + e2->attributes()->set("testAttribute", "testE2"); + e2->attributes()->set("testProtected", "banana", true); + + const QList<Entry*> expectE1{e1.data()}; + const QList<Entry*> expectE2{e2.data()}; + const QList<Entry*> expectBoth{e1.data(), e2.data()}; + + // when not skipping protected, empty term matches everything + m_searchResult = m_entrySearcher.search("", m_rootGroup); + QCOMPARE(m_searchResult, expectBoth); + + // now test the searcher with skipProtected = true + m_entrySearcher = EntrySearcher(false, true); + + // when skipping protected, empty term matches nothing + m_searchResult = m_entrySearcher.search("", m_rootGroup); + QCOMPARE(m_searchResult, {}); + + // having a protected entry in terms should not affect the results in anyways + m_searchResult = m_entrySearcher.search("_testProtected:apple", m_rootGroup); + QCOMPARE(m_searchResult, {}); + m_searchResult = m_entrySearcher.search("_testProtected:apple _testAttribute:testE2", m_rootGroup); + QCOMPARE(m_searchResult, expectE2); + m_searchResult = m_entrySearcher.search("_testProtected:apple _testAttribute:testE1", m_rootGroup); + QCOMPARE(m_searchResult, expectE1); + m_searchResult = + m_entrySearcher.search("_testProtected:apple _testAttribute:testE1 _testAttribute:testE2", m_rootGroup); + QCOMPARE(m_searchResult, {}); + + // also move the protected term around to execurise the short-circut logic + m_searchResult = m_entrySearcher.search("_testAttribute:testE2 _testProtected:apple", m_rootGroup); + QCOMPARE(m_searchResult, expectE2); + m_searchResult = m_entrySearcher.search("_testAttribute:testE1 _testProtected:apple", m_rootGroup); + QCOMPARE(m_searchResult, expectE1); + m_searchResult = + m_entrySearcher.search("_testAttribute:testE1 _testProtected:apple _testAttribute:testE2", m_rootGroup); + QCOMPARE(m_searchResult, {}); +} diff --git a/tests/TestEntrySearcher.h b/tests/TestEntrySearcher.h index 498a00742..4e3e99a43 100644 --- a/tests/TestEntrySearcher.h +++ b/tests/TestEntrySearcher.h @@ -37,6 +37,7 @@ private slots: void testSearchTermParser(); void testCustomAttributesAreSearched(); void testGroup(); + void testSkipProtected(); private: Group* m_rootGroup; diff --git a/tests/data/dbus/interfaces/org.freedesktop.Secret.Collection.xml b/tests/data/dbus/interfaces/org.freedesktop.Secret.Collection.xml new file mode 100644 index 000000000..3b5dd64fd --- /dev/null +++ b/tests/data/dbus/interfaces/org.freedesktop.Secret.Collection.xml @@ -0,0 +1,33 @@ +<interface name="org.freedesktop.Secret.Collection"> + <property name="Items" type="ao" access="read"/> + <property name="Label" type="s" access="readwrite"/> + <property name="Locked" type="b" access="read"/> + <property name="Created" type="t" access="read"/> + <property name="Modified" type="t" access="read"/> + <signal name="ItemCreated"> + <arg name="item" type="o" direction="out"/> + </signal> + <signal name="ItemDeleted"> + <arg name="item" type="o" direction="out"/> + </signal> + <signal name="ItemChanged"> + <arg name="item" type="o" direction="out"/> + </signal> + <method name="Delete"> + <arg type="o" direction="out"/> + </method> + <method name="SearchItems"> + <arg type="ao" direction="out"/> + <arg name="attributes" type="a{ss}" direction="in"/> + <annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="StringStringMap"/> + </method> + <method name="CreateItem"> + <arg type="o" direction="out"/> + <arg name="properties" type="a{sv}" direction="in"/> + <annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="QVariantMap"/> + <arg name="secret" type="(oayays)" direction="in"/> + <annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="FdoSecrets::SecretStruct"/> + <arg name="replace" type="b" direction="in"/> + <arg name="prompt" type="o" direction="out"/> + </method> +</interface> diff --git a/tests/data/dbus/interfaces/org.freedesktop.Secret.Item.xml b/tests/data/dbus/interfaces/org.freedesktop.Secret.Item.xml new file mode 100644 index 000000000..d9c39a2e9 --- /dev/null +++ b/tests/data/dbus/interfaces/org.freedesktop.Secret.Item.xml @@ -0,0 +1,21 @@ +<interface name="org.freedesktop.Secret.Item"> + <property name="Locked" type="b" access="read"/> + <property name="Attributes" type="a{ss}" access="readwrite"> + <annotation name="org.qtproject.QtDBus.QtTypeName" value="StringStringMap"/> + </property> + <property name="Label" type="s" access="readwrite"/> + <property name="Created" type="t" access="read"/> + <property name="Modified" type="t" access="read"/> + <method name="Delete"> + <arg type="o" direction="out"/> + </method> + <method name="GetSecret"> + <arg type="(oayays)" direction="out"/> + <annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="FdoSecrets::SecretStruct"/> + <arg name="session" type="o" direction="in"/> + </method> + <method name="SetSecret"> + <arg name="secret" type="(oayays)" direction="in"/> + <annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="FdoSecrets::SecretStruct"/> + </method> +</interface> diff --git a/tests/data/dbus/interfaces/org.freedesktop.Secret.Prompt.xml b/tests/data/dbus/interfaces/org.freedesktop.Secret.Prompt.xml new file mode 100644 index 000000000..92aa8df84 --- /dev/null +++ b/tests/data/dbus/interfaces/org.freedesktop.Secret.Prompt.xml @@ -0,0 +1,11 @@ +<interface name="org.freedesktop.Secret.Prompt"> + <signal name="Completed"> + <arg name="dismissed" type="b" direction="out"/> + <arg name="result" type="v" direction="out"/> + </signal> + <method name="Prompt"> + <arg name="windowId" type="s" direction="in"/> + </method> + <method name="Dismiss"> + </method> +</interface> diff --git a/tests/data/dbus/interfaces/org.freedesktop.Secret.Service.xml b/tests/data/dbus/interfaces/org.freedesktop.Secret.Service.xml new file mode 100644 index 000000000..40240bb43 --- /dev/null +++ b/tests/data/dbus/interfaces/org.freedesktop.Secret.Service.xml @@ -0,0 +1,55 @@ +<interface name="org.freedesktop.Secret.Service"> + <property name="Collections" type="ao" access="read"/> + <signal name="CollectionCreated"> + <arg name="collection" type="o" direction="out"/> + </signal> + <signal name="CollectionDeleted"> + <arg name="collection" type="o" direction="out"/> + </signal> + <signal name="CollectionChanged"> + <arg name="collection" type="o" direction="out"/> + </signal> + <method name="OpenSession"> + <arg type="v" direction="out"/> + <arg name="algorithm" type="s" direction="in"/> + <arg name="input" type="v" direction="in"/> + <arg name="result" type="o" direction="out"/> + </method> + <method name="CreateCollection"> + <arg type="o" direction="out"/> + <arg name="properties" type="a{sv}" direction="in"/> + <annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="QVariantMap"/> + <arg name="alias" type="s" direction="in"/> + <arg name="prompt" type="o" direction="out"/> + </method> + <method name="SearchItems"> + <arg type="ao" direction="out"/> + <arg name="attributes" type="a{ss}" direction="in"/> + <annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="StringStringMap"/> + <arg name="locked" type="ao" direction="out"/> + </method> + <method name="Unlock"> + <arg type="ao" direction="out"/> + <arg name="paths" type="ao" direction="in"/> + <arg name="prompt" type="o" direction="out"/> + </method> + <method name="Lock"> + <arg type="ao" direction="out"/> + <arg name="paths" type="ao" direction="in"/> + <arg name="prompt" type="o" direction="out"/> + </method> + <method name="GetSecrets"> + <arg type="a{o(oayays)}" direction="out"/> + <annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="ObjectPathSecretMap"/> + <arg name="items" type="ao" direction="in"/> + <arg name="session" type="o" direction="in"/> + </method> + <method name="ReadAlias"> + <arg type="o" direction="out"/> + <arg name="name" type="s" direction="in"/> + </method> + <method name="SetAlias"> + <arg name="name" type="s" direction="in"/> + <arg name="collection" type="o" direction="in"/> + </method> +</interface> diff --git a/tests/data/dbus/interfaces/org.freedesktop.Secret.Session.xml b/tests/data/dbus/interfaces/org.freedesktop.Secret.Session.xml new file mode 100644 index 000000000..7d358df7b --- /dev/null +++ b/tests/data/dbus/interfaces/org.freedesktop.Secret.Session.xml @@ -0,0 +1,4 @@ +<interface name="org.freedesktop.Secret.Session"> + <method name="Close"> + </method> +</interface> diff --git a/tests/data/dbus/session.conf b/tests/data/dbus/session.conf new file mode 100644 index 000000000..096da6d4c --- /dev/null +++ b/tests/data/dbus/session.conf @@ -0,0 +1,39 @@ +<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN" + "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd"> +<busconfig> + <type>session</type> + <keep_umask/> + <listen>unix:tmpdir=/tmp</listen> + <auth>EXTERNAL</auth> + <standard_session_servicedirs /> + <policy context="default"> + <allow send_destination="*" eavesdrop="true"/> + <allow eavesdrop="true"/> + <allow own="*"/> + </policy> + <include ignore_missing="yes">/etc/dbus-1/session.conf</include> + <includedir>session.d</includedir> + <includedir>/etc/dbus-1/session.d</includedir> + <include ignore_missing="yes">/etc/dbus-1/session-local.conf</include> + <include if_selinux_enabled="yes" selinux_root_relative="yes">contexts/dbus_contexts</include> + <limit name="max_incoming_bytes">1000000000</limit> + <limit name="max_incoming_unix_fds">250000000</limit> + <limit name="max_outgoing_bytes">1000000000</limit> + <limit name="max_outgoing_unix_fds">250000000</limit> + <limit name="max_message_size">1000000000</limit> + <limit name="auth_timeout">240000</limit> + <limit name="pending_fd_timeout">150000</limit> + <limit name="max_completed_connections">100000</limit> + <limit name="max_incomplete_connections">10000</limit> + <limit name="max_connections_per_user">100000</limit> + <limit name="max_pending_service_starts">10000</limit> + <limit name="max_names_per_connection">50000</limit> + <limit name="max_match_rules_per_connection">50000</limit> + <limit name="max_replies_per_connection">50000</limit> + <!-- The above is copied from session bus conf. + Our only intent here is to set a low service_start_timeout, + such that ctest can exit sooner when dbus-run-session is used + to launch tests and some service fails to start. + --> + <limit name="service_start_timeout">500</limit> +</busconfig> diff --git a/tests/gui/CMakeLists.txt b/tests/gui/CMakeLists.txt index 6a8d21c4a..1d5822d20 100644 --- a/tests/gui/CMakeLists.txt +++ b/tests/gui/CMakeLists.txt @@ -21,3 +21,12 @@ add_unit_test(NAME testguipixmaps SOURCES TestGuiPixmaps.cpp LIBS ${TEST_LIBRARI if(WITH_XC_BROWSER) add_unit_test(NAME testguibrowser SOURCES TestGuiBrowser.cpp ../util/TemporaryFile.cpp LIBS ${TEST_LIBRARIES}) endif() + +if(WITH_XC_FDOSECRETS) + add_unit_test(NAME testguifdosecrets + SOURCES TestGuiFdoSecrets.cpp ../util/TemporaryFile.cpp + LIBS ${TEST_LIBRARIES} + # The following doesn't work because dbus-run-session expects execname to be in PATH + # dbus-run-session -- execname + LAUNCHER dbus-run-session --config-file ${CMAKE_CURRENT_SOURCE_DIR}/../data/dbus/session.conf -- sh -c "exec ./$0") +endif() diff --git a/tests/gui/TestGuiFdoSecrets.cpp b/tests/gui/TestGuiFdoSecrets.cpp new file mode 100644 index 000000000..d223972c2 --- /dev/null +++ b/tests/gui/TestGuiFdoSecrets.cpp @@ -0,0 +1,1175 @@ +/* + * Copyright (C) 2019 Aetf <aetf@unlimitedcodeworks.xyz> + * + * 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 "TestGuiFdoSecrets.h" + +#include "fdosecrets/FdoSecretsPlugin.h" +#include "fdosecrets/FdoSecretsSettings.h" +#include "fdosecrets/objects/Collection.h" +#include "fdosecrets/objects/Item.h" +#include "fdosecrets/objects/Prompt.h" +#include "fdosecrets/objects/Service.h" +#include "fdosecrets/objects/Session.h" +#include "fdosecrets/objects/SessionCipher.h" + +#include "TestGlobal.h" +#include "config-keepassx-tests.h" + +#include "core/Bootstrap.h" +#include "core/Config.h" +#include "core/Tools.h" +#include "crypto/Crypto.h" +#include "gui/DatabaseTabWidget.h" +#include "gui/DatabaseWidget.h" +#include "gui/FileDialog.h" +#include "gui/MessageBox.h" +#include "gui/wizard/NewDatabaseWizard.h" +#include "util/TemporaryFile.h" + +#include <QAbstractItemView> +#include <QDBusConnection> +#include <QDBusConnectionInterface> +#include <QDBusInterface> +#include <QLineEdit> +#include <QPointer> +#include <QSignalSpy> +#include <QXmlStreamReader> + +#include <memory> +#include <type_traits> + +int main(int argc, char* argv[]) +{ +#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) + QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); +#endif + Application app(argc, argv); + app.setApplicationName("KeePassXC"); + app.setApplicationVersion(KEEPASSXC_VERSION); + app.setQuitOnLastWindowClosed(false); + app.setAttribute(Qt::AA_Use96Dpi, true); + QTEST_DISABLE_KEYPAD_NAVIGATION + TestGuiFdoSecrets tc; + QTEST_SET_MAIN_SOURCE_PATH + return QTest::qExec(&tc, argc, argv); +} + +#define DBUS_PATH_DEFAULT_ALIAS "/org/freedesktop/secrets/aliases/default" + +#define VERIFY(statement) \ + do { \ + if (!QTest::qVerify(static_cast<bool>(statement), #statement, "", __FILE__, __LINE__)) \ + return {}; \ + } while (false) + +#define COMPARE(actual, expected) \ + do { \ + if (!QTest::qCompare(actual, expected, #actual, #expected, __FILE__, __LINE__)) \ + return {}; \ + } while (false) + +#define FAIL(message) \ + do { \ + QTest::qFail(static_cast<const char*>(message), __FILE__, __LINE__); \ + return {}; \ + } while (false) + +#define COMPARE_DBUS_LOCAL_CALL(actual, expected) \ + do { \ + const auto a = (actual); \ + QVERIFY(!a.isError()); \ + QCOMPARE(a.value(), (expected)); \ + } while (false) + +#define CHECKED_DBUS_LOCAL_CALL(name, stmt) \ + std::remove_cv<decltype(stmt)::value_type>::type name; \ + do { \ + const auto rep = stmt; \ + QVERIFY(!rep.isError()); \ + name = rep.value(); \ + } while (false) + +namespace +{ + std::unique_ptr<QDBusInterface> interfaceOf(const QDBusObjectPath& objPath, const QString& interface) + { + std::unique_ptr<QDBusInterface> iface(new QDBusInterface(DBUS_SERVICE_SECRET, objPath.path(), interface)); + iface->setTimeout(5); + VERIFY(iface->isValid()); + return iface; + } + + std::unique_ptr<QDBusInterface> interfaceOf(FdoSecrets::DBusObject* obj) + { + VERIFY(obj); + auto metaAdaptor = obj->dbusAdaptor().metaObject(); + auto ifaceName = metaAdaptor->classInfo(metaAdaptor->indexOfClassInfo("D-Bus Interface")).value(); + + return interfaceOf(obj->objectPath(), ifaceName); + } + + template <typename T> QString extractElement(const QString& doc, T cond) + { + QXmlStreamReader reader(doc); + while (!reader.atEnd()) { + int st = reader.characterOffset(); + + if (reader.readNext() != QXmlStreamReader::StartElement || !cond(reader)) { + continue; + } + + reader.skipCurrentElement(); + if (reader.hasError()) { + break; + } + + // remove whitespaces between elements to be a little bit flexible + int ed = reader.characterOffset(); + return doc.mid(st - 1, ed - st + 1).replace(QRegularExpression(R"(>[\s\n]+<)"), "><"); + } + VERIFY(!reader.hasError()); + return {}; + } + + bool checkDBusSpec(const QString& path, const QString& interface) + { + QFile f(QStringLiteral(KEEPASSX_TEST_DATA_DIR "/dbus/interfaces/%1.xml").arg(interface)); + VERIFY(f.open(QFile::ReadOnly | QFile::Text)); + QTextStream in(&f); + auto spec = in.readAll().replace(QRegularExpression(R"(>[\s\n]+<)"), "><").trimmed(); + + auto bus = QDBusConnection::sessionBus(); + auto msg = QDBusMessage::createMethodCall( + DBUS_SERVICE_SECRET, path, "org.freedesktop.DBus.Introspectable", "Introspect"); + + // BlockWithGui enters event loop + auto reply = QDBusPendingReply<QString>(bus.call(msg, QDBus::BlockWithGui, 5)); + VERIFY(reply.isValid()); + auto actual = extractElement(reply.argumentAt<0>(), [&](const QXmlStreamReader& reader) { + return reader.name() == "interface" && reader.attributes().value("name") == interface; + }); + + COMPARE(actual, spec); + return true; + } +} // namespace + +using namespace FdoSecrets; + +// for use in QSignalSpy +Q_DECLARE_METATYPE(Collection*); +Q_DECLARE_METATYPE(Item*); + +TestGuiFdoSecrets::~TestGuiFdoSecrets() = default; + +void TestGuiFdoSecrets::initTestCase() +{ + // for use in QSignalSpy + qRegisterMetaType<Collection*>(); + qRegisterMetaType<Item*>(); + + QVERIFY(Crypto::init()); + Config::createTempFileInstance(); + config()->set(Config::AutoSaveAfterEveryChange, false); + config()->set(Config::AutoSaveOnExit, false); + config()->set(Config::GUI_ShowTrayIcon, true); + config()->set(Config::UpdateCheckMessageShown, true); + // Disable secret service integration (activate within individual tests to test the plugin) + FdoSecrets::settings()->setEnabled(false); + // activate within individual tests + FdoSecrets::settings()->setShowNotification(false); + + Bootstrap::bootstrapApplication(); + + m_mainWindow.reset(new MainWindow()); + m_tabWidget = m_mainWindow->findChild<DatabaseTabWidget*>("tabWidget"); + QVERIFY(m_tabWidget); + m_plugin = m_mainWindow->findChild<FdoSecretsPlugin*>(); + QVERIFY(m_plugin); + m_mainWindow->show(); + + // Load the NewDatabase.kdbx file into temporary storage + QFile sourceDbFile(QStringLiteral(KEEPASSX_TEST_DATA_DIR "/NewDatabase.kdbx")); + QVERIFY(sourceDbFile.open(QIODevice::ReadOnly)); + QVERIFY(Tools::readAllFromDevice(&sourceDbFile, m_dbData)); + sourceDbFile.close(); + + // set keys for session encryption + m_serverPublic = MpiFromHex("e407997e8b918419cf851cf3345358fdf" + "ffb9564a220ac9c3934efd277cea20d17" + "467ecdc56e817f75ac39501f38a4a04ff" + "64d627e16c09981c7ad876da255b61c8e" + "6a8408236c2a4523cfe6961c26dbdfc77" + "c1a27a5b425ca71a019e829fae32c0b42" + "0e1b3096b48bc2ce9ccab1d1ff13a5eb4" + "b263cee30bdb1a57af9bfa93f"); + m_serverPrivate = MpiFromHex("013f4f3381ef0ca11c4c7363079577b56" + "99b238644e0aba47e24bdba6173590216" + "4f1e12dd0944800a373e090e63192f53b" + "93583e9a9e50bb9d792aafaa3a0f5ae77" + "de0c3423f5820848d88ee3bdd01c889f2" + "7af58a02f5b6693d422b9d189b300d7b1" + "be5076b5795cf8808c31e2e2898368d18" + "ab5c26b0ea3480c9aba8154cf"); + // use the same cipher to do the client side encryption, but exchange the position of client/server keys + m_cipher.reset(new DhIetf1024Sha256Aes128CbcPkcs7); + QVERIFY(m_cipher->initialize(MpiFromBytes(MpiToBytes(m_serverPublic)), + MpiFromHex("30d18c6b328bac970c05bda6af2e708b9" + "d6bbbb6dc136c1a2d96e870fabc86ad74" + "1846a26a4197f32f65ea2e7580ad2afe3" + "dd5d6c1224b8368b0df2cd75d520a9ff9" + "7fe894cc7da71b7bd285b4633359c16c8" + "d341f822fa4f0fdf59b5d3448658c46a2" + "a86dbb14ff85823873f8a259ccc52bbb8" + "2b5a4c2a75447982553b42221"), + MpiFromHex("84aafe9c9f356f7762307f4d791acb59e" + "8e3fd562abdbb481d0587f8400ad6c51d" + "af561a1beb9a22c8cd4d2807367c5787b" + "2e06d631ccbb5194b6bb32211583ce688" + "f9c2cebc22a9e4d494d12ebdd570c61a1" + "62a94e88561d25ccd0415339d1f59e1b0" + "6bc6b6b5fde46e23b2410eb034be390d3" + "2407ec7ae90f0831f24afd5ac"))); +} + +// Every test starts with opening the temp database +void TestGuiFdoSecrets::init() +{ + m_dbFile.reset(new TemporaryFile()); + // Write the temp storage to a temp database file for use in our tests + QVERIFY(m_dbFile->open()); + QCOMPARE(m_dbFile->write(m_dbData), static_cast<qint64>((m_dbData.size()))); + m_dbFile->close(); + + // make sure window is activated or focus tests may fail + m_mainWindow->activateWindow(); + QApplication::processEvents(); + + // open and unlock the database + m_tabWidget->addDatabaseTab(m_dbFile->fileName(), false, "a"); + m_dbWidget = m_tabWidget->currentDatabaseWidget(); + m_db = m_dbWidget->database(); + + // by default expsoe the root group + FdoSecrets::settings()->setExposedGroup(m_db, m_db->rootGroup()->uuid()); + QVERIFY(m_dbWidget->save()); +} + +// Every test ends with closing the temp database without saving +void TestGuiFdoSecrets::cleanup() +{ + // restore to default settings + FdoSecrets::settings()->setShowNotification(false); + FdoSecrets::settings()->setEnabled(false); + if (m_plugin) { + m_plugin->updateServiceState(); + } + + // DO NOT save the database + for (int i = 0; i != m_tabWidget->count(); ++i) { + m_tabWidget->databaseWidgetFromIndex(i)->database()->markAsClean(); + } + QVERIFY(m_tabWidget->closeAllDatabaseTabs()); + QApplication::processEvents(); + + if (m_dbFile) { + m_dbFile->remove(); + } +} + +void TestGuiFdoSecrets::cleanupTestCase() +{ + if (m_dbFile) { + m_dbFile->remove(); + } +} + +void TestGuiFdoSecrets::testDBusSpec() +{ + auto service = enableService(); + QVERIFY(service); + + // service + QCOMPARE(service->objectPath().path(), QStringLiteral(DBUS_PATH_SECRETS)); + QVERIFY(checkDBusSpec(service->objectPath().path(), DBUS_INTERFACE_SECRET_SERVICE)); + + // default alias + QVERIFY(checkDBusSpec(DBUS_PATH_DEFAULT_ALIAS, DBUS_INTERFACE_SECRET_COLLECTION)); + + // collection + auto coll = getDefaultCollection(service); + QVERIFY(coll); + QVERIFY(checkDBusSpec(coll->objectPath().path(), DBUS_INTERFACE_SECRET_COLLECTION)); + + // item + auto item = getFirstItem(coll); + QVERIFY(item); + QVERIFY(checkDBusSpec(item->objectPath().path(), DBUS_INTERFACE_SECRET_ITEM)); + + // session + auto sess = openSession(service, PlainCipher::Algorithm); + QVERIFY(sess); + QVERIFY(checkDBusSpec(sess->objectPath().path(), DBUS_INTERFACE_SECRET_SESSION)); + + // prompt + FdoSecrets::settings()->setNoConfirmDeleteItem(true); + PromptBase* prompt = nullptr; + { + auto rep = item->deleteItem(); + QVERIFY(!rep.isError()); + prompt = rep.value(); + } + QVERIFY(prompt); + QVERIFY(checkDBusSpec(prompt->objectPath().path(), DBUS_INTERFACE_SECRET_PROMPT)); +} + +void TestGuiFdoSecrets::testServiceEnable() +{ + QSignalSpy sigError(m_plugin, SIGNAL(error(QString))); + QVERIFY(sigError.isValid()); + + QSignalSpy sigStarted(m_plugin, SIGNAL(secretServiceStarted())); + QVERIFY(sigStarted.isValid()); + + // make sure no one else is holding the service + QVERIFY(!QDBusConnection::sessionBus().interface()->isServiceRegistered(DBUS_SERVICE_SECRET)); + + // enable the service + auto service = enableService(); + QVERIFY(service); + + // service started without error + QVERIFY(sigError.isEmpty()); + QCOMPARE(sigStarted.size(), 1); + + QApplication::processEvents(); + + QVERIFY(QDBusConnection::sessionBus().interface()->isServiceRegistered(DBUS_SERVICE_SECRET)); + + // there will be one default collection + auto coll = getDefaultCollection(service); + QVERIFY(coll); + + COMPARE_DBUS_LOCAL_CALL(coll->locked(), false); + COMPARE_DBUS_LOCAL_CALL(coll->label(), m_db->metadata()->name()); + COMPARE_DBUS_LOCAL_CALL( + coll->created(), + static_cast<qulonglong>(m_db->rootGroup()->timeInfo().creationTime().toMSecsSinceEpoch() / 1000)); + COMPARE_DBUS_LOCAL_CALL( + coll->modified(), + static_cast<qulonglong>(m_db->rootGroup()->timeInfo().lastModificationTime().toMSecsSinceEpoch() / 1000)); +} + +void TestGuiFdoSecrets::testServiceEnableNoExposedDatabase() +{ + // reset the exposed group and then enable the service + FdoSecrets::settings()->setExposedGroup(m_db, {}); + auto service = enableService(); + QVERIFY(service); + + // no collections + COMPARE_DBUS_LOCAL_CALL(service->collections(), QList<Collection*>{}); +} + +void TestGuiFdoSecrets::testServiceSearch() +{ + auto service = enableService(); + QVERIFY(service); + auto coll = getDefaultCollection(service); + QVERIFY(coll); + auto item = getFirstItem(coll); + QVERIFY(item); + + item->backend()->attributes()->set("fdosecrets-test", "1"); + item->backend()->attributes()->set("fdosecrets-test-protected", "2", true); + const QString crazyKey = "_a:bc&-+'-e%12df_d"; + const QString crazyValue = "[v]al@-ue"; + item->backend()->attributes()->set(crazyKey, crazyValue); + + // search by title + { + QList<Item*> locked; + CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"Title", item->backend()->title()}}, locked)); + QCOMPARE(locked.size(), 0); + QCOMPARE(unlocked, {item}); + } + + // search by attribute + { + QList<Item*> locked; + CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"fdosecrets-test", "1"}}, locked)); + QCOMPARE(locked.size(), 0); + QCOMPARE(unlocked, {item}); + } + { + QList<Item*> locked; + CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{crazyKey, crazyValue}}, locked)); + QCOMPARE(locked.size(), 0); + QCOMPARE(unlocked, {item}); + } + + // searching using empty terms returns nothing + { + QList<Item*> locked; + CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({}, locked)); + QCOMPARE(locked.size(), 0); + QCOMPARE(unlocked.size(), 0); + } + + // searching using protected attributes or password returns nothing + { + QList<Item*> locked; + CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"Password", item->backend()->password()}}, locked)); + QCOMPARE(locked.size(), 0); + QCOMPARE(unlocked.size(), 0); + } + { + QList<Item*> locked; + CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"fdosecrets-test-protected", "2"}}, locked)); + QCOMPARE(locked.size(), 0); + QCOMPARE(unlocked.size(), 0); + } +} + +void TestGuiFdoSecrets::testServiceUnlock() +{ + lockDatabaseInBackend(); + + auto service = enableService(); + QVERIFY(service); + auto coll = getDefaultCollection(service); + QVERIFY(coll); + + QSignalSpy spyCollectionCreated(service, SIGNAL(collectionCreated(Collection*))); + QVERIFY(spyCollectionCreated.isValid()); + QSignalSpy spyCollectionDeleted(service, SIGNAL(collectionDeleted(Collection*))); + QVERIFY(spyCollectionDeleted.isValid()); + QSignalSpy spyCollectionChanged(service, SIGNAL(collectionChanged(Collection*))); + QVERIFY(spyCollectionChanged.isValid()); + + PromptBase* prompt = nullptr; + { + CHECKED_DBUS_LOCAL_CALL(unlocked, service->unlock({coll.data()}, prompt)); + // nothing is unlocked immediately without user's action + QVERIFY(unlocked.isEmpty()); + } + QVERIFY(prompt); + QSignalSpy spyPromptCompleted(prompt, SIGNAL(completed(bool, QVariant))); + QVERIFY(spyPromptCompleted.isValid()); + + // nothing is unlocked yet + QCOMPARE(spyPromptCompleted.count(), 0); + QVERIFY(coll); + QVERIFY(coll->backend()->isLocked()); + + // drive the prompt + QVERIFY(!prompt->prompt("").isError()); + + // still not unlocked before user action + QCOMPARE(spyPromptCompleted.count(), 0); + QVERIFY(coll); + QVERIFY(coll->backend()->isLocked()); + + // interact with the dialog + QApplication::processEvents(); + { + auto dbOpenDlg = m_tabWidget->findChild<DatabaseOpenDialog*>(); + QVERIFY(dbOpenDlg); + auto editPassword = dbOpenDlg->findChild<QLineEdit*>("editPassword"); + QVERIFY(editPassword); + editPassword->setFocus(); + QTest::keyClicks(editPassword, "a"); + QTest::keyClick(editPassword, Qt::Key_Enter); + } + QApplication::processEvents(); + + // unlocked + QVERIFY(coll); + QVERIFY(!coll->backend()->isLocked()); + + QCOMPARE(spyPromptCompleted.count(), 1); + { + auto args = spyPromptCompleted.takeFirst(); + QCOMPARE(args.size(), 2); + QCOMPARE(args.at(0).toBool(), false); + QCOMPARE(args.at(1).value<QList<QDBusObjectPath>>(), {coll->objectPath()}); + } + QCOMPARE(spyCollectionCreated.count(), 0); + QCOMPARE(spyCollectionChanged.count(), 1); + { + auto args = spyCollectionChanged.takeFirst(); + QCOMPARE(args.size(), 1); + QCOMPARE(args.at(0).value<Collection*>(), coll.data()); + } + QCOMPARE(spyCollectionDeleted.count(), 0); +} + +void TestGuiFdoSecrets::testServiceLock() +{ + auto service = enableService(); + QVERIFY(service); + auto coll = getDefaultCollection(service); + QVERIFY(coll); + + QSignalSpy spyCollectionCreated(service, SIGNAL(collectionCreated(Collection*))); + QVERIFY(spyCollectionCreated.isValid()); + QSignalSpy spyCollectionDeleted(service, SIGNAL(collectionDeleted(Collection*))); + QVERIFY(spyCollectionDeleted.isValid()); + QSignalSpy spyCollectionChanged(service, SIGNAL(collectionChanged(Collection*))); + QVERIFY(spyCollectionChanged.isValid()); + + // if the db is modified, prompt user + m_db->markAsModified(); + { + PromptBase* prompt = nullptr; + CHECKED_DBUS_LOCAL_CALL(locked, service->lock({coll}, prompt)); + QCOMPARE(locked.size(), 0); + QVERIFY(prompt); + QSignalSpy spyPromptCompleted(prompt, SIGNAL(completed(bool, QVariant))); + QVERIFY(spyPromptCompleted.isValid()); + + // prompt and click cancel + MessageBox::setNextAnswer(MessageBox::Cancel); + QVERIFY(!prompt->prompt("").isError()); + QApplication::processEvents(); + + QVERIFY(!coll->backend()->isLocked()); + + QCOMPARE(spyPromptCompleted.count(), 1); + auto args = spyPromptCompleted.takeFirst(); + QCOMPARE(args.count(), 2); + QCOMPARE(args.at(0).toBool(), true); + QCOMPARE(args.at(1).value<QList<QDBusObjectPath>>(), {}); + } + { + PromptBase* prompt = nullptr; + CHECKED_DBUS_LOCAL_CALL(locked, service->lock({coll}, prompt)); + QCOMPARE(locked.size(), 0); + QVERIFY(prompt); + QSignalSpy spyPromptCompleted(prompt, SIGNAL(completed(bool, QVariant))); + QVERIFY(spyPromptCompleted.isValid()); + + // prompt and click save + MessageBox::setNextAnswer(MessageBox::Save); + QVERIFY(!prompt->prompt("").isError()); + QApplication::processEvents(); + + QVERIFY(coll->backend()->isLocked()); + + QCOMPARE(spyPromptCompleted.count(), 1); + auto args = spyPromptCompleted.takeFirst(); + QCOMPARE(args.count(), 2); + QCOMPARE(args.at(0).toBool(), false); + QCOMPARE(args.at(1).value<QList<QDBusObjectPath>>(), {coll->objectPath()}); + } + + QCOMPARE(spyCollectionCreated.count(), 0); + QCOMPARE(spyCollectionChanged.count(), 1); + { + auto args = spyCollectionChanged.takeFirst(); + QCOMPARE(args.size(), 1); + QCOMPARE(args.at(0).value<Collection*>(), coll.data()); + } + QCOMPARE(spyCollectionDeleted.count(), 0); + + // locking item locks the whole db + unlockDatabaseInBackend(); + { + auto item = getFirstItem(coll); + PromptBase* prompt = nullptr; + CHECKED_DBUS_LOCAL_CALL(locked, service->lock({item}, prompt)); + QCOMPARE(locked.size(), 0); + QVERIFY(prompt); + + MessageBox::setNextAnswer(MessageBox::Save); + QVERIFY(!prompt->prompt("").isError()); + QApplication::processEvents(); + + QVERIFY(coll->backend()->isLocked()); + } +} + +void TestGuiFdoSecrets::testSessionOpen() +{ + auto service = enableService(); + QVERIFY(service); + + auto sess = openSession(service, PlainCipher::Algorithm); + QVERIFY(sess); + QCOMPARE(service->sessions().size(), 1); + + sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm); + QVERIFY(sess); + QCOMPARE(service->sessions().size(), 2); +} + +void TestGuiFdoSecrets::testSessionClose() +{ + auto service = enableService(); + QVERIFY(service); + + auto sess = openSession(service, PlainCipher::Algorithm); + QVERIFY(sess); + + QCOMPARE(service->sessions().size(), 1); + + auto rep = sess->close(); + QVERIFY(!rep.isError()); + + QCOMPARE(service->sessions().size(), 0); +} + +void TestGuiFdoSecrets::testCollectionCreate() +{ + auto service = enableService(); + QVERIFY(service); + + QSignalSpy spyCollectionCreated(service, SIGNAL(collectionCreated(Collection*))); + QVERIFY(spyCollectionCreated.isValid()); + + // returns existing if alias is nonempty and exists + { + PromptBase* prompt = nullptr; + CHECKED_DBUS_LOCAL_CALL( + coll, service->createCollection({{DBUS_INTERFACE_SECRET_COLLECTION ".Label", "NewDB"}}, "default", prompt)); + QVERIFY(!prompt); + QCOMPARE(coll, getDefaultCollection(service).data()); + } + QCOMPARE(spyCollectionCreated.count(), 0); + + // create new one and set properties + { + PromptBase* prompt = nullptr; + CHECKED_DBUS_LOCAL_CALL( + created, + service->createCollection({{DBUS_INTERFACE_SECRET_COLLECTION ".Label", "Test NewDB"}}, "mydatadb", prompt)); + QVERIFY(!created); + QVERIFY(prompt); + + QSignalSpy spyPromptCompleted(prompt, SIGNAL(completed(bool, QVariant))); + QVERIFY(spyPromptCompleted.isValid()); + + QTimer::singleShot(50, this, SLOT(createDatabaseCallback())); + QVERIFY(!prompt->prompt("").isError()); + QApplication::processEvents(); + + QCOMPARE(spyPromptCompleted.count(), 1); + auto args = spyPromptCompleted.takeFirst(); + QCOMPARE(args.size(), 2); + QCOMPARE(args.at(0).toBool(), false); + auto coll = FdoSecrets::pathToObject<Collection>(args.at(1).value<QDBusObjectPath>()); + QVERIFY(coll); + + QCOMPARE(coll->backend()->database()->metadata()->name(), QStringLiteral("Test NewDB")); + + QCOMPARE(spyCollectionCreated.count(), 1); + { + auto args = spyCollectionCreated.takeFirst(); + QCOMPARE(args.size(), 1); + QCOMPARE(args.at(0).value<Collection*>(), coll); + } + } +} + +void TestGuiFdoSecrets::createDatabaseCallback() +{ + auto wizard = m_tabWidget->findChild<NewDatabaseWizard*>(); + QVERIFY(wizard); + + QCOMPARE(wizard->currentId(), 0); + wizard->next(); + wizard->next(); + QCOMPARE(wizard->currentId(), 2); + + // enter password + auto* passwordEdit = wizard->findChild<QLineEdit*>("enterPasswordEdit"); + auto* passwordRepeatEdit = wizard->findChild<QLineEdit*>("repeatPasswordEdit"); + QTest::keyClicks(passwordEdit, "test"); + QTest::keyClick(passwordEdit, Qt::Key::Key_Tab); + QTest::keyClicks(passwordRepeatEdit, "test"); + + // save database to temporary file + TemporaryFile tmpFile; + QVERIFY(tmpFile.open()); + tmpFile.close(); + fileDialog()->setNextFileName(tmpFile.fileName()); + + wizard->accept(); + + tmpFile.remove(); +} + +void TestGuiFdoSecrets::testCollectionDelete() +{ + auto service = enableService(); + QVERIFY(service); + auto coll = getDefaultCollection(service); + QVERIFY(coll); + // closing the tab calls coll->deleteLater() + // but deleteLater is not processed in QApplication::processEvent + // see https://doc.qt.io/qt-5/qcoreapplication.html#processEvents + auto rawColl = coll.data(); + + QSignalSpy spyCollectionDeleted(service, SIGNAL(collectionDeleted(Collection*))); + QVERIFY(spyCollectionDeleted.isValid()); + + m_db->markAsModified(); + CHECKED_DBUS_LOCAL_CALL(prompt, coll->deleteCollection()); + QVERIFY(prompt); + QSignalSpy spyPromptCompleted(prompt, SIGNAL(completed(bool, QVariant))); + QVERIFY(spyPromptCompleted.isValid()); + + // prompt and click save + MessageBox::setNextAnswer(MessageBox::Save); + QVERIFY(!prompt->prompt("").isError()); + + QApplication::processEvents(); + + // closing the tab should have deleted coll if not in testing + // but deleteLater is not processed in QApplication::processEvent + // see https://doc.qt.io/qt-5/qcoreapplication.html#processEvents + // QVERIFY(!coll); + + QCOMPARE(spyPromptCompleted.count(), 1); + auto args = spyPromptCompleted.takeFirst(); + QCOMPARE(args.count(), 2); + QCOMPARE(args.at(0).toBool(), false); + QCOMPARE(args.at(1).toString(), QStringLiteral("")); + + QCOMPARE(spyCollectionDeleted.count(), 1); + { + auto args = spyCollectionDeleted.takeFirst(); + QCOMPARE(args.size(), 1); + QCOMPARE(args.at(0).value<Collection*>(), rawColl); + } +} + +void TestGuiFdoSecrets::testItemCreate() +{ + auto service = enableService(); + QVERIFY(service); + auto coll = getDefaultCollection(service); + QVERIFY(coll); + auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm); + QVERIFY(sess); + + // create item + StringStringMap attributes{ + {"application", "fdosecrets-test"}, + {"attr-i[bute]", "![some] -value*"}, + }; + + auto item = createItem(sess, coll, "abc", "Password", attributes, false); + QVERIFY(item); + + // attributes + { + CHECKED_DBUS_LOCAL_CALL(actual, item->attributes()); + for (const auto& key : attributes.keys()) { + QVERIFY(actual.contains(key)); + QCOMPARE(actual[key], attributes[key]); + } + } + + // label + COMPARE_DBUS_LOCAL_CALL(item->label(), QStringLiteral("abc")); + + // secrets + { + CHECKED_DBUS_LOCAL_CALL(ss, item->getSecret(sess)); + ss = m_cipher->decrypt(ss); + QCOMPARE(ss.value, QByteArray("Password")); + } + + // searchable + { + QList<Item*> locked; + CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems(attributes, locked)); + QCOMPARE(locked, QList<Item*>{}); + QCOMPARE(unlocked, QList<Item*>{item}); + } + { + CHECKED_DBUS_LOCAL_CALL(unlocked, coll->searchItems(attributes)); + QVERIFY(unlocked.contains(item)); + } +} + +void TestGuiFdoSecrets::testItemReplace() +{ + auto service = enableService(); + QVERIFY(service); + auto coll = getDefaultCollection(service); + QVERIFY(coll); + auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm); + QVERIFY(sess); + + // create item + StringStringMap attr1{ + {"application", "fdosecrets-test"}, + {"attr-i[bute]", "![some] -value*"}, + {"fdosecrets-attr", "1"}, + }; + StringStringMap attr2{ + {"application", "fdosecrets-test"}, + {"attr-i[bute]", "![some] -value*"}, + {"fdosecrets-attr", "2"}, + }; + + auto item1 = createItem(sess, coll, "abc1", "Password", attr1, false); + QVERIFY(item1); + auto item2 = createItem(sess, coll, "abc2", "Password", attr2, false); + QVERIFY(item2); + + { + QList<Item*> locked; + CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"application", "fdosecrets-test"}}, locked)); + QCOMPARE(unlocked.size(), 2); + } + + { + // when replace, existing item with matching attr is updated + auto item3 = createItem(sess, coll, "abc3", "Password", attr2, true); + QVERIFY(item3); + QCOMPARE(item2, item3); + COMPARE_DBUS_LOCAL_CALL(item3->label(), QStringLiteral("abc3")); + // there is still 2 entries + QList<Item*> locked; + CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"application", "fdosecrets-test"}}, locked)); + QCOMPARE(unlocked.size(), 2); + } + + { + // when NOT replace, another entry is created + auto item4 = createItem(sess, coll, "abc4", "Password", attr2, false); + QVERIFY(item4); + COMPARE_DBUS_LOCAL_CALL(item2->label(), QStringLiteral("abc3")); + COMPARE_DBUS_LOCAL_CALL(item4->label(), QStringLiteral("abc4")); + // there is 3 entries + QList<Item*> locked; + CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"application", "fdosecrets-test"}}, locked)); + QCOMPARE(unlocked.size(), 3); + } +} + +void TestGuiFdoSecrets::testItemSecret() +{ + const QString TEXT_PLAIN = "text/plain"; + const QString APPLICATION_OCTET_STREAM = "application/octet-stream"; + + auto service = enableService(); + QVERIFY(service); + auto coll = getDefaultCollection(service); + QVERIFY(coll); + auto item = getFirstItem(coll); + QVERIFY(item); + auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm); + QVERIFY(sess); + + // plain text secret + { + CHECKED_DBUS_LOCAL_CALL(encrypted, item->getSecret(sess)); + auto ss = m_cipher->decrypt(encrypted); + QCOMPARE(ss.contentType, TEXT_PLAIN); + QCOMPARE(ss.value, item->backend()->password().toUtf8()); + } + + // get secret with notification (only works when called from DBUS) + FdoSecrets::settings()->setShowNotification(true); + { + QSignalSpy spyShowNotification(m_plugin, SIGNAL(requestShowNotification(QString, QString, int))); + QVERIFY(spyShowNotification.isValid()); + + auto iitem = interfaceOf(item); + QVERIFY(static_cast<bool>(iitem)); + + auto replyMsg = iitem->call(QDBus::BlockWithGui, "GetSecret", QVariant::fromValue(sess->objectPath())); + auto reply = QDBusPendingReply<SecretStruct>(replyMsg); + QVERIFY(reply.isValid()); + auto ss = m_cipher->decrypt(reply.argumentAt<0>()); + + QCOMPARE(ss.contentType, TEXT_PLAIN); + QCOMPARE(ss.value, item->backend()->password().toUtf8()); + + QCOMPARE(spyShowNotification.count(), 1); + } + FdoSecrets::settings()->setShowNotification(false); + + // set secret with plain text + { + SecretStruct ss; + ss.contentType = TEXT_PLAIN; + ss.value = "NewPassword"; + ss.session = sess->objectPath(); + QVERIFY(!item->setSecret(m_cipher->encrypt(ss)).isError()); + + QCOMPARE(item->backend()->password().toUtf8(), ss.value); + } + + // set secret with something else is saved as attachment + { + SecretStruct expected; + expected.contentType = APPLICATION_OCTET_STREAM; + expected.value = "NewPasswordBinary"; + expected.session = sess->objectPath(); + QVERIFY(!item->setSecret(m_cipher->encrypt(expected)).isError()); + + QCOMPARE(item->backend()->password(), QStringLiteral("")); + + CHECKED_DBUS_LOCAL_CALL(encrypted, item->getSecret(sess)); + auto ss = m_cipher->decrypt(encrypted); + QCOMPARE(ss.contentType, expected.contentType); + QCOMPARE(ss.value, expected.value); + } +} + +void TestGuiFdoSecrets::testItemDelete() +{ + FdoSecrets::settings()->setNoConfirmDeleteItem(false); + + auto service = enableService(); + QVERIFY(service); + auto coll = getDefaultCollection(service); + QVERIFY(coll); + auto item = getFirstItem(coll); + QVERIFY(item); + auto rawItem = item.data(); + + QSignalSpy spyItemDeleted(coll, SIGNAL(itemDeleted(Item*))); + QVERIFY(spyItemDeleted.isValid()); + + CHECKED_DBUS_LOCAL_CALL(prompt, item->deleteItem()); + QVERIFY(prompt); + + QSignalSpy spyPromptCompleted(prompt, SIGNAL(completed(bool, QVariant))); + QVERIFY(spyPromptCompleted.isValid()); + + // prompt and click save + if (item->isDeletePermanent()) { + MessageBox::setNextAnswer(MessageBox::Delete); + } else { + MessageBox::setNextAnswer(MessageBox::Move); + } + QVERIFY(!prompt->prompt("").isError()); + + QApplication::processEvents(); + + QCOMPARE(spyPromptCompleted.count(), 1); + auto args = spyPromptCompleted.takeFirst(); + QCOMPARE(args.count(), 2); + QCOMPARE(args.at(0).toBool(), false); + QCOMPARE(args.at(1).toString(), QStringLiteral("")); + + QCOMPARE(spyItemDeleted.count(), 1); + { + auto args = spyItemDeleted.takeFirst(); + QCOMPARE(args.size(), 1); + QCOMPARE(args.at(0).value<Item*>(), rawItem); + } +} + +void TestGuiFdoSecrets::testAlias() +{ + auto service = enableService(); + QVERIFY(service); + + // read default alias + CHECKED_DBUS_LOCAL_CALL(coll, service->readAlias("default")); + QVERIFY(coll); + + // set extra alias + QVERIFY(!service->setAlias("another", coll).isError()); + + // get using extra alias + CHECKED_DBUS_LOCAL_CALL(coll2, service->readAlias("another")); + QVERIFY(coll2); + QCOMPARE(coll, coll2); +} + +void TestGuiFdoSecrets::testDefaultAliasAlwaysPresent() +{ + auto service = enableService(); + QVERIFY(service); + + // one collection, which is default alias + auto coll = getDefaultCollection(service); + QVERIFY(coll); + + // after locking, the collection is still there, but locked + lockDatabaseInBackend(); + + coll = getDefaultCollection(service); + QVERIFY(coll); + COMPARE_DBUS_LOCAL_CALL(coll->locked(), true); + + // unlock the database, the alias and collection is present + unlockDatabaseInBackend(); + + coll = getDefaultCollection(service); + QVERIFY(coll); + COMPARE_DBUS_LOCAL_CALL(coll->locked(), false); +} + +void TestGuiFdoSecrets::testExposeSubgroup() +{ + auto subgroup = m_db->rootGroup()->findGroupByPath("/Homebanking/Subgroup"); + QVERIFY(subgroup); + FdoSecrets::settings()->setExposedGroup(m_db, subgroup->uuid()); + auto service = enableService(); + QVERIFY(service); + + auto coll = getDefaultCollection(service); + QVERIFY(coll); + + // exposing subgroup does not expose entries in other groups + auto items = coll->items(); + QVERIFY(!items.isError()); + QList<Entry*> exposedEntries; + for (const auto& item : items.value()) { + exposedEntries << item->backend(); + } + QCOMPARE(exposedEntries, subgroup->entries()); +} + +void TestGuiFdoSecrets::testModifiyingExposedGroup() +{ + // test when exposed group is removed the collection is not exposed anymore + auto subgroup = m_db->rootGroup()->findGroupByPath("/Homebanking"); + QVERIFY(subgroup); + FdoSecrets::settings()->setExposedGroup(m_db, subgroup->uuid()); + auto service = enableService(); + QVERIFY(service); + + { + CHECKED_DBUS_LOCAL_CALL(colls, service->collections()); + QCOMPARE(colls.size(), 1); + } + + m_db->metadata()->setRecycleBinEnabled(true); + m_db->recycleGroup(subgroup); + QApplication::processEvents(); + + { + CHECKED_DBUS_LOCAL_CALL(colls, service->collections()); + QCOMPARE(colls.size(), 0); + } + + // test setting another exposed group, the collection will be exposed again + FdoSecrets::settings()->setExposedGroup(m_db, m_db->rootGroup()->uuid()); + QApplication::processEvents(); + { + CHECKED_DBUS_LOCAL_CALL(colls, service->collections()); + QCOMPARE(colls.size(), 1); + } +} + +QPointer<Service> TestGuiFdoSecrets::enableService() +{ + FdoSecrets::settings()->setEnabled(true); + VERIFY(m_plugin); + m_plugin->updateServiceState(); + return m_plugin->serviceInstance(); +} + +QPointer<Session> TestGuiFdoSecrets::openSession(Service* service, const QString& algo) +{ + // open session has to be called actually over DBUS to get peer info + + VERIFY(service); + auto iservice = interfaceOf(service); + VERIFY(iservice); + + if (algo == PlainCipher::Algorithm) { + auto replyMsg = iservice->call(QDBus::BlockWithGui, "OpenSession", algo, QVariant::fromValue(QDBusVariant(""))); + auto reply = QDBusPendingReply<QDBusVariant, QDBusObjectPath>(replyMsg); + + VERIFY(reply.isValid()); + return FdoSecrets::pathToObject<Session>(reply.argumentAt<1>()); + } else if (algo == DhIetf1024Sha256Aes128CbcPkcs7::Algorithm) { + + DhIetf1024Sha256Aes128CbcPkcs7::fixNextServerKeys(MpiFromBytes(MpiToBytes(m_serverPrivate)), + MpiFromBytes(MpiToBytes(m_serverPublic))); + + auto replyMsg = iservice->call( + QDBus::BlockWithGui, "OpenSession", algo, QVariant::fromValue(QDBusVariant(m_cipher->m_publicKey))); + auto reply = QDBusPendingReply<QDBusVariant, QDBusObjectPath>(replyMsg); + VERIFY(reply.isValid()); + COMPARE(qvariant_cast<QByteArray>(reply.argumentAt<0>().variant()), MpiToBytes(m_serverPublic)); + return FdoSecrets::pathToObject<Session>(reply.argumentAt<1>()); + } + FAIL("Unsupported algorithm"); +} + +QPointer<Collection> TestGuiFdoSecrets::getDefaultCollection(Service* service) +{ + VERIFY(service); + auto coll = service->readAlias("default"); + VERIFY(!coll.isError()); + return coll.value(); +} + +QPointer<Item> TestGuiFdoSecrets::getFirstItem(Collection* coll) +{ + VERIFY(coll); + auto items = coll->items(); + VERIFY(!items.isError()); + VERIFY(!items.value().isEmpty()); + return items.value().at(0); +} + +QPointer<Item> TestGuiFdoSecrets::createItem(Session* sess, + Collection* coll, + const QString& label, + const QString& pass, + const StringStringMap& attr, + bool replace) +{ + VERIFY(sess); + VERIFY(coll); + + QVariantMap properties{ + {DBUS_INTERFACE_SECRET_ITEM ".Label", QVariant::fromValue(label)}, + {DBUS_INTERFACE_SECRET_ITEM ".Attributes", QVariant::fromValue(attr)}, + }; + + SecretStruct ss; + ss.session = sess->objectPath(); + ss.value = pass.toLocal8Bit(); + ss.contentType = "plain/text"; + ss = m_cipher->encrypt(ss); + + PromptBase* prompt = nullptr; + auto item = coll->createItem(properties, ss, replace, prompt); + VERIFY(!item.isError()); + // creating item does not have a prompt to show + VERIFY(!prompt); + return item.value(); +} + +void TestGuiFdoSecrets::lockDatabaseInBackend() +{ + m_dbWidget->lock(); + m_db.reset(); + QApplication::processEvents(); +} + +void TestGuiFdoSecrets::unlockDatabaseInBackend() +{ + m_dbWidget->performUnlockDatabase("a"); + m_db = m_dbWidget->database(); + QApplication::processEvents(); +} diff --git a/tests/gui/TestGuiFdoSecrets.h b/tests/gui/TestGuiFdoSecrets.h new file mode 100644 index 000000000..8a961eb8e --- /dev/null +++ b/tests/gui/TestGuiFdoSecrets.h @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2019 Aetf <aetf@unlimitedcodeworks.xyz> + * + * 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/>. + */ + +#ifndef KEEPASSXC_TESTGUIFDOSECRETS_H +#define KEEPASSXC_TESTGUIFDOSECRETS_H + +#include <QByteArray> +#include <QObject> +#include <QPointer> +#include <QScopedPointer> +#include <QSharedPointer> +#include <QString> + +#include "fdosecrets/GcryptMPI.h" +#include "fdosecrets/objects/DBusTypes.h" + +class MainWindow; +class Database; +class DatabaseTabWidget; +class DatabaseWidget; +class TemporaryFile; +class FdoSecretsPlugin; +namespace FdoSecrets +{ + class Service; + class Session; + class Collection; + class Item; + class Prompt; + class DhIetf1024Sha256Aes128CbcPkcs7; +} // namespace FdoSecrets + +class QAbstractItemView; + +class TestGuiFdoSecrets : public QObject +{ + Q_OBJECT + +public: + ~TestGuiFdoSecrets() override; + +private slots: + void initTestCase(); + void init(); + void cleanup(); + void cleanupTestCase(); + + void testDBusSpec(); + + void testServiceEnable(); + void testServiceEnableNoExposedDatabase(); + void testServiceSearch(); + void testServiceUnlock(); + void testServiceLock(); + + void testSessionOpen(); + void testSessionClose(); + + void testCollectionCreate(); + void testCollectionDelete(); + + void testItemCreate(); + void testItemReplace(); + void testItemSecret(); + void testItemDelete(); + + void testAlias(); + void testDefaultAliasAlwaysPresent(); + + void testExposeSubgroup(); + void testModifiyingExposedGroup(); + +protected slots: + void createDatabaseCallback(); + +private: + void lockDatabaseInBackend(); + void unlockDatabaseInBackend(); + QPointer<FdoSecrets::Service> enableService(); + QPointer<FdoSecrets::Session> openSession(FdoSecrets::Service* service, const QString& algo); + QPointer<FdoSecrets::Collection> getDefaultCollection(FdoSecrets::Service* service); + QPointer<FdoSecrets::Item> getFirstItem(FdoSecrets::Collection* coll); + QPointer<FdoSecrets::Item> createItem(FdoSecrets::Session* sess, + FdoSecrets::Collection* coll, + const QString& label, + const QString& pass, + const StringStringMap& attr, + bool replace); + +private: + QScopedPointer<MainWindow> m_mainWindow; + QPointer<DatabaseTabWidget> m_tabWidget; + QPointer<DatabaseWidget> m_dbWidget; + QSharedPointer<Database> m_db; + + QPointer<FdoSecretsPlugin> m_plugin; + + // For DH session tests + GcryptMPI m_serverPrivate; + GcryptMPI m_serverPublic; + std::unique_ptr<FdoSecrets::DhIetf1024Sha256Aes128CbcPkcs7> m_cipher; + + QByteArray m_dbData; + QScopedPointer<TemporaryFile> m_dbFile; +}; + +#endif // KEEPASSXC_TESTGUIFDOSECRETS_H |